diff --git a/.goreleaser.template.yml b/.goreleaser.template.yml index 581b32ac5..a328d09df 100644 --- a/.goreleaser.template.yml +++ b/.goreleaser.template.yml @@ -140,5 +140,3 @@ release: kubectl apply -f https://github.com/operator-framework/rukpak/releases/download/{{ .Tag }}/rukpak.yaml kubectl wait --for=condition=Available --namespace=rukpak-system deployment/core --timeout=60s kubectl wait --for=condition=Available --namespace=rukpak-system deployment/rukpak-webhooks --timeout=60s - kubectl wait --for=condition=Available --namespace=crdvalidator-system deployment/crd-validation-webhook --timeout=60s - ``` diff --git a/Makefile b/Makefile index e54022144..ce3aa5c52 100644 --- a/Makefile +++ b/Makefile @@ -165,6 +165,7 @@ kind-load-bundles: kind ## Load the e2e testdata container images into a kind cl $(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/plain-v0/invalid-crds-and-crs -t testdata/bundles/plain-v0:invalid-crds-and-crs $(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/plain-v0/subdir -t testdata/bundles/plain-v0:subdir $(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry/valid -t testdata/bundles/registry:valid + $(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry/invalid -t testdata/bundles/registry:invalid $(KIND) load docker-image testdata/bundles/plain-v0:valid --name $(KIND_CLUSTER_NAME) $(KIND) load docker-image testdata/bundles/plain-v0:dependent --name $(KIND_CLUSTER_NAME) $(KIND) load docker-image testdata/bundles/plain-v0:provides --name $(KIND_CLUSTER_NAME) @@ -174,6 +175,7 @@ kind-load-bundles: kind ## Load the e2e testdata container images into a kind cl $(KIND) load docker-image testdata/bundles/plain-v0:invalid-crds-and-crs --name $(KIND_CLUSTER_NAME) $(KIND) load docker-image testdata/bundles/plain-v0:subdir --name $(KIND_CLUSTER_NAME) $(KIND) load docker-image testdata/bundles/registry:valid --name $(KIND_CLUSTER_NAME) + $(KIND) load docker-image testdata/bundles/registry:invalid --name $(KIND_CLUSTER_NAME) kind-load: kind ## Loads the currently constructed image onto the cluster $(KIND) load docker-image $(IMAGE) --name $(KIND_CLUSTER_NAME) diff --git a/cmd/rukpakctl/cmd/bundle.go b/cmd/rukpakctl/cmd/bundle.go index ae690cef2..ccfabd55b 100644 --- a/cmd/rukpakctl/cmd/bundle.go +++ b/cmd/rukpakctl/cmd/bundle.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/cmd/rukpakctl/cmd/bundledeployment.go b/cmd/rukpakctl/cmd/bundledeployment.go index d4835f522..26e25d381 100644 --- a/cmd/rukpakctl/cmd/bundledeployment.go +++ b/cmd/rukpakctl/cmd/bundledeployment.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/cmd/rukpakctl/cmd/content.go b/cmd/rukpakctl/cmd/content.go index e1ac1d20f..607879014 100644 --- a/cmd/rukpakctl/cmd/content.go +++ b/cmd/rukpakctl/cmd/content.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/cmd/rukpakctl/cmd/create.go b/cmd/rukpakctl/cmd/create.go index 0f56a6502..26959cef9 100644 --- a/cmd/rukpakctl/cmd/create.go +++ b/cmd/rukpakctl/cmd/create.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/cmd/rukpakctl/cmd/root.go b/cmd/rukpakctl/cmd/root.go index ec9fc6db8..5e71ac974 100644 --- a/cmd/rukpakctl/cmd/root.go +++ b/cmd/rukpakctl/cmd/root.go @@ -1,5 +1,6 @@ /* Copyright © 2022 NAME HERE + */ package cmd diff --git a/cmd/rukpakctl/cmd/run.go b/cmd/rukpakctl/cmd/run.go index e7fd06cad..d326d743c 100644 --- a/cmd/rukpakctl/cmd/run.go +++ b/cmd/rukpakctl/cmd/run.go @@ -1,5 +1,6 @@ /* Copyright © 2022 NAME HERE + */ package cmd diff --git a/cmd/rukpakctl/main.go b/cmd/rukpakctl/main.go index 3ec462874..7b0c96858 100644 --- a/cmd/rukpakctl/main.go +++ b/cmd/rukpakctl/main.go @@ -1,5 +1,6 @@ /* Copyright © 2022 NAME HERE + */ package main diff --git a/cmd/rukpakctl/utils/utils.go b/cmd/rukpakctl/utils/utils.go index c31bb79e1..f753f720c 100644 --- a/cmd/rukpakctl/utils/utils.go +++ b/cmd/rukpakctl/utils/utils.go @@ -1,10 +1,12 @@ /* Copyright © 2022 NAME HERE + */ package utils import ( "context" + "io/ioutil" "os" "path/filepath" @@ -23,7 +25,7 @@ func CreateConfigmap(ctx context.Context, core typedv1.CoreV1Interface, name, di if info.IsDir() { return nil } - c, err := os.ReadFile(path) + c, err := ioutil.ReadFile(path) if err != nil { return err } diff --git a/docs/sources/http.md b/docs/sources/http.md new file mode 100644 index 000000000..9d948db78 --- /dev/null +++ b/docs/sources/http.md @@ -0,0 +1,74 @@ +# Http source + +## Summary + +The http source provides a compressed archive file (`tgz` format) downloadable by the http protocol as the source of the bundle. +The `source.type` for the http source is `http`. When creating a http source, a URL of the compressed archive file must be specified. +It is expected that a proper format of bundle content is present +in the compressed archive file. + +## Example + +Referencing a compressed archive file in a github repository release archive: + +```yaml +apiVersion: core.rukpak.io/v1alpha1 +kind: BundleDeployment +metadata: + name: my-ahoy +spec: + provisionerClassName: core-rukpak-io-helm + template: + metadata: + labels: + app: my-ahoy + spec: + provisionerClassName: core-rukpak-io-helm + source: + http: + url: https://github.com/helm/examples/releases/download/hello-world-0.1.0/hello-world-0.1.0.tgz + type: http +``` + +## Authorization + +An http source can provide authorization for access to private compressed archives by creating a secret in the namespace that the provisioner is deployed. +The secret used in the http source is based around [Basic authentication secret](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret) +and is expected to contain `data.username` and `data.password` for the username and password, respectively. +If the `http.auth.secret.insecureSkipVerify` is set true, the download operation will accept any certificate presented by the server and any host name in that +certificate. In this mode, TLS is susceptible to machine-in-the-middle attacks unless custom verification is +used. This should be used only for testing. + +### Example with authorization + +1. Create the secret + +```sh +kubectl create secret generic accesssecret --type "kubernetes.io/basic-auth" --from-literal=username=myusername --from-literal=password=mypassword -n rukpak-system +``` + +2. Create a bundle deployment referencing a private compressed archive file: + +```bash +kubectl apply -f -< Must ensure nothing is stored at removed version -// 2. New CRD changes a version that Old CRD has => Must validate existing CRs with new schema -// 3. New CRD adds a version that Old CRD does not have => -// - If conversion strategy is None, ensure existing CRs validate with new schema. -// - If conversion strategy is Webhook, allow update (assume webhook handles conversion correctly) +// 1. New CRD removes version that Old CRD had => Must ensure nothing is stored at removed version +// 2. New CRD changes a version that Old CRD has => Must validate existing CRs with new schema +// 3. New CRD adds a version that Old CRD does not have => +// - If conversion strategy is None, ensure existing CRs validate with new schema. +// - If conversion strategy is Webhook, allow update (assume webhook handles conversion correctly) func validateCRDCompatibility(ctx context.Context, cl client.Client, oldCRD *apiextensionsv1.CustomResourceDefinition, newCRD *apiextensionsv1.CustomResourceDefinition) error { oldVersions := map[string]apiextensionsv1.CustomResourceDefinitionVersion{} newVersions := map[string]apiextensionsv1.CustomResourceDefinitionVersion{} diff --git a/internal/source/git.go b/internal/source/git.go index d63b56de4..e8ba15963 100644 --- a/internal/source/git.go +++ b/internal/source/git.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "strings" + "syscall" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" @@ -107,10 +108,19 @@ func (r *Git) Unpack(ctx context.Context, bundle *rukpakv1alpha1.Bundle) (*Resul bundleFS = &billyFS{sub} } + commitHash, err := repo.ResolveRevision("HEAD") + if err != nil { + return nil, fmt.Errorf("resolve commit hash: %v", err) + } + + resolvedGit := bundle.Spec.Source.Git.DeepCopy() + resolvedGit.Ref = rukpakv1alpha1.GitRef{ + Commit: commitHash.String(), + } + resolvedSource := &rukpakv1alpha1.BundleSource{ Type: rukpakv1alpha1.SourceTypeGit, - // TODO: improve git source implementation to return result with commit hash. - Git: bundle.Spec.Source.Git.DeepCopy(), + Git: resolvedGit, } return &Result{Bundle: bundleFS, ResolvedSource: resolvedSource, State: StateUnpacked}, nil @@ -209,12 +219,18 @@ func (f *billyFS) ReadFile(name string) ([]byte, error) { } func (f *billyFS) Open(path string) (fs.File, error) { + fi, err := f.Filesystem.Stat(path) + if err != nil { + return nil, err + } + if fi.IsDir() { + return &billyDirFile{billyFile{nil, fi}, f, path}, nil + } file, err := f.Filesystem.Open(path) if err != nil { return nil, err } - fi, err := f.Filesystem.Stat(path) - return &billyFile{file, fi, err}, nil + return &billyFile{file, fi}, nil } func (f *billyFS) ReadDir(name string) ([]fs.DirEntry, error) { @@ -231,10 +247,34 @@ func (f *billyFS) ReadDir(name string) ([]fs.DirEntry, error) { type billyFile struct { billy.File - fi os.FileInfo - fiErr error + fi os.FileInfo } func (b billyFile) Stat() (fs.FileInfo, error) { - return b.fi, b.fiErr + return b.fi, nil +} + +func (b billyFile) Close() error { + if b.File == nil { + return nil + } + return b.File.Close() +} + +type billyDirFile struct { + billyFile + fs *billyFS + path string +} + +func (d *billyDirFile) ReadDir(n int) ([]fs.DirEntry, error) { + entries, err := d.fs.ReadDir(d.path) + if n <= 0 || n > len(entries) { + n = len(entries) + } + return entries[:n], err +} + +func (d billyDirFile) Read(data []byte) (int, error) { + return 0, &fs.PathError{Op: "read", Path: d.path, Err: syscall.EISDIR} } diff --git a/internal/source/image.go b/internal/source/image.go index 1f5e316d5..d7cbb365e 100644 --- a/internal/source/image.go +++ b/internal/source/image.go @@ -12,7 +12,11 @@ import ( "github.com/nlepage/go-tarfs" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + applyconfigurationcorev1 "k8s.io/client-go/applyconfigurations/core/v1" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -61,59 +65,122 @@ func (i *Image) Unpack(ctx context.Context, bundle *rukpakv1alpha1.Bundle) (*Res } func (i *Image) ensureUnpackPod(ctx context.Context, bundle *rukpakv1alpha1.Bundle, pod *corev1.Pod) (controllerutil.OperationResult, error) { - controllerRef := metav1.NewControllerRef(bundle, bundle.GroupVersionKind()) - automountServiceAccountToken := false - pod.SetName(bundle.Name) - pod.SetNamespace(i.PodNamespace) - - return util.CreateOrRecreate(ctx, i.Client, pod, func() error { - pod.SetLabels(map[string]string{ - util.CoreOwnerKindKey: bundle.Kind, - util.CoreOwnerNameKey: bundle.Name, - }) - pod.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) - pod.Spec.AutomountServiceAccountToken = &automountServiceAccountToken - pod.Spec.RestartPolicy = corev1.RestartPolicyNever + existingPod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: i.PodNamespace, Name: bundle.Name}} + if err := i.Client.Get(ctx, client.ObjectKeyFromObject(existingPod), existingPod); client.IgnoreNotFound(err) != nil { + return controllerutil.OperationResultNone, err + } - if len(pod.Spec.InitContainers) != 1 { - pod.Spec.InitContainers = make([]corev1.Container, 1) + podApplyConfig := i.getDesiredPodApplyConfig(bundle) + updatedPod, err := i.KubeClient.CoreV1().Pods(i.PodNamespace).Apply(ctx, podApplyConfig, metav1.ApplyOptions{Force: true, FieldManager: "rukpak-core"}) + if err != nil { + if !apierrors.IsInvalid(err) { + return controllerutil.OperationResultNone, err } + if err := i.Client.Delete(ctx, existingPod); err != nil { + return controllerutil.OperationResultNone, err + } + updatedPod, err = i.KubeClient.CoreV1().Pods(i.PodNamespace).Apply(ctx, podApplyConfig, metav1.ApplyOptions{Force: true, FieldManager: "rukpak-core"}) + if err != nil { + return controllerutil.OperationResultNone, err + } + } - pod.Spec.InitContainers[0].Name = "install-unpacker" - pod.Spec.InitContainers[0].Image = i.UnpackImage - pod.Spec.InitContainers[0].ImagePullPolicy = corev1.PullIfNotPresent - pod.Spec.InitContainers[0].Command = []string{"cp", "-Rv", "/unpack", "/bin/unpack"} - pod.Spec.InitContainers[0].VolumeMounts = []corev1.VolumeMount{{Name: "util", MountPath: "/bin"}} + // make sure the passed in pod value is updated with the latest + // version of the pod + *pod = *updatedPod - if len(pod.Spec.Containers) != 1 { - pod.Spec.Containers = make([]corev1.Container, 1) - } + // compare existingPod to newPod and return an appropriate + // OperatorResult value. + newPod := updatedPod.DeepCopy() + unsetNonComparedPodFields(existingPod, newPod) + if equality.Semantic.DeepEqual(existingPod, newPod) { + return controllerutil.OperationResultNone, nil + } + return controllerutil.OperationResultUpdated, nil +} - pod.Spec.Containers[0].Name = imageBundleUnpackContainerName - pod.Spec.Containers[0].Image = bundle.Spec.Source.Image.Ref - pod.Spec.Containers[0].Command = []string{"/bin/unpack", "--bundle-dir", "/"} - pod.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{{Name: "util", MountPath: "/bin"}} +func (i *Image) getDesiredPodApplyConfig(bundle *rukpakv1alpha1.Bundle) *applyconfigurationcorev1.PodApplyConfiguration { + // TODO (tyslaton): Address unpacker pod allowing root users for image sources + // + // In our current implementation, we are creating a pod that uses the image + // provided by an image source. This pod is not always guaranteed to run as a + // non-root user and thus will fail to initialize if running as root in a PSA + // restricted namespace due to violations. As it currently stands, our compliance + // with PSA is baseline which allows for pods to run as root users. However, + // all RukPak processes and resources, except this unpacker pod for image sources, + // are runnable in a PSA restricted environment. We should consider ways to make + // this PSA definition either configurable or workable in a restricted namespace. + // + // See https://github.com/operator-framework/rukpak/pull/539 for more detail. + containerSecurityContext := applyconfigurationcorev1.SecurityContext(). + WithAllowPrivilegeEscalation(false). + WithCapabilities(applyconfigurationcorev1.Capabilities(). + WithDrop("ALL"), + ) - if bundle.Spec.Source.Image.ImagePullSecretName != "" { - pod.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{Name: bundle.Spec.Source.Image.ImagePullSecretName}} - } - pod.Spec.Volumes = []corev1.Volume{ - {Name: "util", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, - } - return nil - }) + podApply := applyconfigurationcorev1.Pod(bundle.Name, i.PodNamespace). + WithLabels(map[string]string{ + util.CoreOwnerKindKey: bundle.Kind, + util.CoreOwnerNameKey: bundle.Name, + }). + WithOwnerReferences(v1.OwnerReference(). + WithName(bundle.Name). + WithKind(bundle.Kind). + WithAPIVersion(bundle.APIVersion). + WithUID(bundle.UID). + WithController(true). + WithBlockOwnerDeletion(true), + ). + WithSpec(applyconfigurationcorev1.PodSpec(). + WithAutomountServiceAccountToken(false). + WithRestartPolicy(corev1.RestartPolicyNever). + WithInitContainers(applyconfigurationcorev1.Container(). + WithName("install-unpacker"). + WithImage(i.UnpackImage). + WithImagePullPolicy(corev1.PullIfNotPresent). + WithCommand("cp", "-Rv", "/unpack", "/bin/unpack"). + WithVolumeMounts(applyconfigurationcorev1.VolumeMount(). + WithName("util"). + WithMountPath("/bin"), + ). + WithSecurityContext(containerSecurityContext), + ). + WithContainers(applyconfigurationcorev1.Container(). + WithName(imageBundleUnpackContainerName). + WithImage(bundle.Spec.Source.Image.Ref). + WithCommand("/bin/unpack", "--bundle-dir", "/"). + WithVolumeMounts(applyconfigurationcorev1.VolumeMount(). + WithName("util"). + WithMountPath("/bin"), + ). + WithSecurityContext(containerSecurityContext), + ). + WithVolumes(applyconfigurationcorev1.Volume(). + WithName("util"). + WithEmptyDir(applyconfigurationcorev1.EmptyDirVolumeSource()), + ). + WithSecurityContext(applyconfigurationcorev1.PodSecurityContext(). + WithRunAsNonRoot(false). + WithSeccompProfile(applyconfigurationcorev1.SeccompProfile(). + WithType(corev1.SeccompProfileTypeRuntimeDefault), + ), + ), + ) + + if bundle.Spec.Source.Image.ImagePullSecretName != "" { + podApply.Spec = podApply.Spec.WithImagePullSecrets( + applyconfigurationcorev1.LocalObjectReference().WithName(bundle.Spec.Source.Image.ImagePullSecretName), + ) + } + return podApply } -func pendingImagePodResult(pod *corev1.Pod) *Result { - var messages []string - for _, cStatus := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) { - if waiting := cStatus.State.Waiting; waiting != nil { - if waiting.Reason == "ErrImagePull" || waiting.Reason == "ImagePullBackOff" { - messages = append(messages, waiting.Message) - } - } +func unsetNonComparedPodFields(pods ...*corev1.Pod) { + for _, p := range pods { + p.APIVersion = "" + p.Kind = "" + p.Status = corev1.PodStatus{} } - return &Result{State: StatePending, Message: strings.Join(messages, "; ")} } func (i *Image) failedPodResult(ctx context.Context, pod *corev1.Pod) error { @@ -190,3 +257,15 @@ func (i *Image) handleUnexpectedPod(ctx context.Context, pod *corev1.Pod) error _ = i.Client.Delete(ctx, pod) return fmt.Errorf("unexpected pod phase: %v", pod.Status.Phase) } + +func pendingImagePodResult(pod *corev1.Pod) *Result { + var messages []string + for _, cStatus := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) { + if waiting := cStatus.State.Waiting; waiting != nil { + if waiting.Reason == "ErrImagePull" || waiting.Reason == "ImagePullBackOff" { + messages = append(messages, waiting.Message) + } + } + } + return &Result{State: StatePending, Message: strings.Join(messages, "; ")} +} diff --git a/internal/util/util.go b/internal/util/util.go index d0dc5f349..0a523aa96 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "hash/fnv" + "io/ioutil" "os" "sort" "time" @@ -22,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -116,13 +118,14 @@ type ProvisionerClassNameGetter interface { // matches the provided provisionerClassName, this handler enqueues a request for that owner to be reconciled. func MapOwneeToOwnerProvisionerHandler(ctx context.Context, cl client.Client, log logr.Logger, provisionerClassName string, owner ProvisionerClassNameGetter) handler.EventHandler { return handler.EnqueueRequestsFromMapFunc(func(obj client.Object) []reconcile.Request { - gvks, unversioned, err := cl.Scheme().ObjectKinds(owner) + ownerGVK, err := apiutil.GVKForObject(owner, cl.Scheme()) if err != nil { - log.Error(err, "get GVKs for owner") + log.Error(err, "map ownee to owner: lookup GVK for owner") return nil } - if unversioned { - log.Error(err, "owner cannot be an unversioned type") + owneeGVK, err := apiutil.GVKForObject(obj, cl.Scheme()) + if err != nil { + log.Error(err, "map ownee to owner: lookup GVK for ownee") return nil } @@ -132,29 +135,33 @@ func MapOwneeToOwnerProvisionerHandler(ctx context.Context, cl client.Client, lo } var oi *ownerInfo - refLoop: for _, ref := range obj.GetOwnerReferences() { gv, err := schema.ParseGroupVersion(ref.APIVersion) if err != nil { - log.Error(err, fmt.Sprintf("parse group version %q", ref.APIVersion)) + log.Error(err, fmt.Sprintf("map ownee to owner: parse ownee's owner reference group version %q", ref.APIVersion)) return nil } refGVK := gv.WithKind(ref.Kind) - for _, gvk := range gvks { - if refGVK == gvk && ref.Controller != nil && *ref.Controller { - oi = &ownerInfo{ - key: types.NamespacedName{Name: ref.Name}, - gvk: gvk, - } - break refLoop + if refGVK == ownerGVK && ref.Controller != nil && *ref.Controller { + oi = &ownerInfo{ + key: types.NamespacedName{Name: ref.Name}, + gvk: ownerGVK, } + break } } if oi == nil { return nil } - if err := cl.Get(ctx, oi.key, owner); err != nil { - log.Error(err, "get owner", "kind", oi.gvk, "name", oi.key.Name) + + if err := cl.Get(ctx, oi.key, owner); client.IgnoreNotFound(err) != nil { + log.Info("map ownee to owner: get owner", + "ownee", client.ObjectKeyFromObject(obj), + "owneeKind", owneeGVK, + "owner", oi.key, + "ownerKind", oi.gvk, + "error", err.Error(), + ) return nil } if owner.ProvisionerClassName() != provisionerClassName { @@ -294,7 +301,7 @@ func SortBundlesByCreation(bundles *rukpakv1alpha1.BundleList) { // automatically for Pods at runtime. If that file doesn't exist, then // return the @defaultNamespace namespace parameter. func PodNamespace(defaultNamespace string) string { - namespace, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + namespace, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") if err != nil { return defaultNamespace } diff --git a/manifests/apis/webhooks/resources/deployment.yaml b/manifests/apis/webhooks/resources/deployment.yaml index ed93a6a8b..ef9feee4e 100644 --- a/manifests/apis/webhooks/resources/deployment.yaml +++ b/manifests/apis/webhooks/resources/deployment.yaml @@ -15,9 +15,17 @@ spec: labels: app: webhooks spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault serviceAccountName: webhooks-admin containers: - name: webhooks + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ "ALL" ] command: ["/webhooks"] image: quay.io/operator-framework/rukpak:latest imagePullPolicy: IfNotPresent diff --git a/manifests/apis/webhooks/resources/namespace.yaml b/manifests/apis/webhooks/resources/namespace.yaml index 1ab3a7255..e600766fd 100644 --- a/manifests/apis/webhooks/resources/namespace.yaml +++ b/manifests/apis/webhooks/resources/namespace.yaml @@ -1,4 +1,7 @@ apiVersion: v1 kind: Namespace metadata: + labels: + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/enforce-version: latest name: system diff --git a/manifests/core/resources/deployment.yaml b/manifests/core/resources/deployment.yaml index f10f04eba..3c283d491 100644 --- a/manifests/core/resources/deployment.yaml +++ b/manifests/core/resources/deployment.yaml @@ -18,8 +18,16 @@ spec: kubectl.kubernetes.io/default-container: manager spec: serviceAccountName: core-admin + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault containers: - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ "ALL" ] image: quay.io/brancz/kube-rbac-proxy:v0.12.0 args: - "--secure-listen-address=0.0.0.0:8443" @@ -37,6 +45,10 @@ spec: - name: certs mountPath: /etc/pki/tls - name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ "ALL" ] image: quay.io/operator-framework/rukpak:latest imagePullPolicy: IfNotPresent command: ["/core"] diff --git a/manifests/provisioners/helm/resources/deployment.yaml b/manifests/provisioners/helm/resources/deployment.yaml index 30600a155..437b82e28 100644 --- a/manifests/provisioners/helm/resources/deployment.yaml +++ b/manifests/provisioners/helm/resources/deployment.yaml @@ -17,9 +17,17 @@ spec: annotations: kubectl.kubernetes.io/default-container: manager spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault serviceAccountName: helm-provisioner-admin containers: - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ "ALL" ] image: quay.io/brancz/kube-rbac-proxy:v0.12.0 args: - "--secure-listen-address=0.0.0.0:8443" @@ -37,6 +45,10 @@ spec: - name: certs mountPath: /etc/pki/tls - name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ "ALL" ] image: quay.io/operator-framework/rukpak:latest imagePullPolicy: IfNotPresent command: ["/helm"] diff --git a/test/e2e/helm_provisioner_test.go b/test/e2e/helm_provisioner_test.go index 4982ce92e..6958b8c92 100644 --- a/test/e2e/helm_provisioner_test.go +++ b/test/e2e/helm_provisioner_test.go @@ -432,6 +432,69 @@ var _ = Describe("helm provisioner bundledeployment", func() { }) }) }) + When("a BundleDeployment targets a valid Bundle with no chart directory in Github", func() { + var ( + bd *rukpakv1alpha1.BundleDeployment + ctx context.Context + ) + BeforeEach(func() { + ctx = context.Background() + + bd = &rukpakv1alpha1.BundleDeployment{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "ahoy-", + }, + Spec: rukpakv1alpha1.BundleDeploymentSpec{ + ProvisionerClassName: helm.ProvisionerID, + Template: &rukpakv1alpha1.BundleTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/name": "ahoy", + }, + }, + Spec: rukpakv1alpha1.BundleSpec{ + ProvisionerClassName: helm.ProvisionerID, + Source: rukpakv1alpha1.BundleSource{ + Type: rukpakv1alpha1.SourceTypeGit, + Git: &rukpakv1alpha1.GitSource{ + Repository: "https://github.com/helm/examples", + Directory: "./charts/hello-world", + Ref: rukpakv1alpha1.GitRef{ + Branch: "main", + }, + }, + }, + }, + }, + }, + } + err := c.Create(ctx, bd) + Expect(err).To(BeNil()) + }) + AfterEach(func() { + By("deleting the testing resources") + Expect(c.Delete(ctx, bd)).To(BeNil()) + }) + + It("should rollout the bundle contents successfully", func() { + By("eventually writing a successful installation state back to the bundledeployment status") + Eventually(func() (*metav1.Condition, error) { + if err := c.Get(ctx, client.ObjectKeyFromObject(bd), bd); err != nil { + return nil, err + } + if bd.Status.ActiveBundle == "" { + return nil, fmt.Errorf("waiting for bundle name to be populated") + } + return meta.FindStatusCondition(bd.Status.Conditions, rukpakv1alpha1.TypeInstalled), nil + }).Should(And( + Not(BeNil()), + WithTransform(func(c *metav1.Condition) string { return c.Type }, Equal(rukpakv1alpha1.TypeInstalled)), + WithTransform(func(c *metav1.Condition) metav1.ConditionStatus { return c.Status }, Equal(metav1.ConditionTrue)), + WithTransform(func(c *metav1.Condition) string { return c.Reason }, Equal(rukpakv1alpha1.ReasonInstallationSucceeded)), + WithTransform(func(c *metav1.Condition) string { return c.Message }, ContainSubstring("instantiated bundle")), + )) + }) + }) When("a BundleDeployment targets a valid Bundle with values", func() { var ( bd *rukpakv1alpha1.BundleDeployment diff --git a/test/e2e/plain_provisioner_test.go b/test/e2e/plain_provisioner_test.go index a53cf2c6e..d7acee14d 100644 --- a/test/e2e/plain_provisioner_test.go +++ b/test/e2e/plain_provisioner_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "net/http" "os" "path/filepath" @@ -533,24 +534,42 @@ var _ = Describe("plain provisioner bundle", func() { }) It("Can create and unpack the bundle successfully", func() { - Eventually(func() error { - if err := c.Get(ctx, client.ObjectKeyFromObject(bundle), bundle); err != nil { - return err - } - if bundle.Status.Phase != rukpakv1alpha1.PhaseUnpacked { - return errors.New("bundle is not unpacked") - } - - provisionerPods := &corev1.PodList{} - if err := c.List(context.Background(), provisionerPods, client.MatchingLabels{"app": "core"}); err != nil { - return err - } - if len(provisionerPods.Items) != 1 { - return errors.New("expected exactly 1 provisioner pod") - } + By("eventually unpacking the bundle", func() { + Eventually(func() error { + if err := c.Get(ctx, client.ObjectKeyFromObject(bundle), bundle); err != nil { + return err + } + if bundle.Status.Phase != rukpakv1alpha1.PhaseUnpacked { + return errors.New("bundle is not unpacked") + } + + provisionerPods := &corev1.PodList{} + if err := c.List(context.Background(), provisionerPods, client.MatchingLabels{"app": "core"}); err != nil { + return err + } + if len(provisionerPods.Items) != 1 { + return errors.New("expected exactly 1 provisioner pod") + } + + return checkProvisionerBundle(bundle, provisionerPods.Items[0].Name) + }).Should(BeNil()) + }) - return checkProvisionerBundle(bundle, provisionerPods.Items[0].Name) - }).Should(BeNil()) + By("eventually writing a non-empty commit hash to the status", func() { + Eventually(func() (*rukpakv1alpha1.BundleSource, error) { + if err := c.Get(ctx, client.ObjectKeyFromObject(bundle), bundle); err != nil { + return nil, err + } + return bundle.Status.ResolvedSource, nil + }).Should(And( + Not(BeNil()), + WithTransform(func(s *rukpakv1alpha1.BundleSource) rukpakv1alpha1.SourceType { return s.Type }, Equal(rukpakv1alpha1.SourceTypeGit)), + WithTransform(func(s *rukpakv1alpha1.BundleSource) *rukpakv1alpha1.GitSource { return s.Git }, And( + Not(BeNil()), + WithTransform(func(i *rukpakv1alpha1.GitSource) string { return i.Ref.Commit }, Not(Equal(""))), + )), + )) + }) }) }) @@ -586,24 +605,42 @@ var _ = Describe("plain provisioner bundle", func() { }) It("Can create and unpack the bundle successfully", func() { - Eventually(func() error { - if err := c.Get(ctx, client.ObjectKeyFromObject(bundle), bundle); err != nil { - return err - } - if bundle.Status.Phase != rukpakv1alpha1.PhaseUnpacked { - return errors.New("bundle is not unpacked") - } - - provisionerPods := &corev1.PodList{} - if err := c.List(context.Background(), provisionerPods, client.MatchingLabels{"app": "core"}); err != nil { - return err - } - if len(provisionerPods.Items) != 1 { - return errors.New("expected exactly 1 provisioner pod") - } + By("eventually unpacking the bundle", func() { + Eventually(func() error { + if err := c.Get(ctx, client.ObjectKeyFromObject(bundle), bundle); err != nil { + return err + } + if bundle.Status.Phase != rukpakv1alpha1.PhaseUnpacked { + return errors.New("bundle is not unpacked") + } + + provisionerPods := &corev1.PodList{} + if err := c.List(context.Background(), provisionerPods, client.MatchingLabels{"app": "core"}); err != nil { + return err + } + if len(provisionerPods.Items) != 1 { + return errors.New("expected exactly 1 provisioner pod") + } + + return checkProvisionerBundle(bundle, provisionerPods.Items[0].Name) + }).Should(BeNil()) + }) - return checkProvisionerBundle(bundle, provisionerPods.Items[0].Name) - }).Should(BeNil()) + By("eventually writing a non-empty commit hash to the status", func() { + Eventually(func() (*rukpakv1alpha1.BundleSource, error) { + if err := c.Get(ctx, client.ObjectKeyFromObject(bundle), bundle); err != nil { + return nil, err + } + return bundle.Status.ResolvedSource, nil + }).Should(And( + Not(BeNil()), + WithTransform(func(s *rukpakv1alpha1.BundleSource) rukpakv1alpha1.SourceType { return s.Type }, Equal(rukpakv1alpha1.SourceTypeGit)), + WithTransform(func(s *rukpakv1alpha1.BundleSource) *rukpakv1alpha1.GitSource { return s.Git }, And( + Not(BeNil()), + WithTransform(func(i *rukpakv1alpha1.GitSource) string { return i.Ref.Commit }, Not(Equal(""))), + )), + )) + }) }) }) @@ -760,7 +797,7 @@ var _ = Describe("plain provisioner bundle", func() { if info.IsDir() { return nil } - c, err := os.ReadFile(path) + c, err := ioutil.ReadFile(path) if err != nil { return err } @@ -898,7 +935,7 @@ var _ = Describe("plain provisioner bundle", func() { if info.IsDir() { return nil } - c, err := os.ReadFile(path) + c, err := ioutil.ReadFile(path) if err != nil { return err } diff --git a/test/e2e/registry_provisioner_test.go b/test/e2e/registry_provisioner_test.go index 416266ec0..9e43b2ec9 100644 --- a/test/e2e/registry_provisioner_test.go +++ b/test/e2e/registry_provisioner_test.go @@ -75,4 +75,59 @@ var _ = Describe("registry provisioner bundle", func() { )) }) }) + When("a BundleDeployment targets an invalid registry+v1 Bundle", func() { + var ( + bd *rukpakv1alpha1.BundleDeployment + ctx context.Context + ) + BeforeEach(func() { + ctx = context.Background() + + bd = &rukpakv1alpha1.BundleDeployment{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "cincinnati", + }, + Spec: rukpakv1alpha1.BundleDeploymentSpec{ + ProvisionerClassName: plain.ProvisionerID, + Template: &rukpakv1alpha1.BundleTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/name": "cincinnati", + }, + }, + Spec: rukpakv1alpha1.BundleSpec{ + ProvisionerClassName: registry.ProvisionerID, + Source: rukpakv1alpha1.BundleSource{ + Type: rukpakv1alpha1.SourceTypeImage, + Image: &rukpakv1alpha1.ImageSource{ + Ref: "testdata/bundles/registry:invalid", + }, + }, + }, + }, + }, + } + err := c.Create(ctx, bd) + Expect(err).To(BeNil()) + }) + AfterEach(func() { + By("deleting the testing BI resource") + Expect(c.Delete(ctx, bd)).To(BeNil()) + }) + + It("should eventually write a failed conversion state to the bundledeployment status", func() { + Eventually(func() (*metav1.Condition, error) { + if err := c.Get(ctx, client.ObjectKeyFromObject(bd), bd); err != nil { + return nil, err + } + return meta.FindStatusCondition(bd.Status.Conditions, rukpakv1alpha1.TypeHasValidBundle), nil + }).Should(And( + Not(BeNil()), + WithTransform(func(c *metav1.Condition) string { return c.Type }, Equal(rukpakv1alpha1.TypeHasValidBundle)), + WithTransform(func(c *metav1.Condition) metav1.ConditionStatus { return c.Status }, Equal(metav1.ConditionFalse)), + WithTransform(func(c *metav1.Condition) string { return c.Reason }, Equal(rukpakv1alpha1.ReasonUnpackFailed)), + WithTransform(func(c *metav1.Condition) string { return c.Message }, ContainSubstring("convert registry+v1 bundle to plain+v0 bundle: AllNamespace install mode must be enabled")), + )) + }) + }) }) diff --git a/testdata/bundles/registry/invalid/Dockerfile b/testdata/bundles/registry/invalid/Dockerfile new file mode 100644 index 000000000..113ec8b0f --- /dev/null +++ b/testdata/bundles/registry/invalid/Dockerfile @@ -0,0 +1,3 @@ +FROM scratch +COPY manifests /manifests +COPY metadata /metadata diff --git a/testdata/bundles/registry/invalid/manifests/update-service-operator.clusterserviceversion.yaml b/testdata/bundles/registry/invalid/manifests/update-service-operator.clusterserviceversion.yaml new file mode 100644 index 000000000..4de987f5d --- /dev/null +++ b/testdata/bundles/registry/invalid/manifests/update-service-operator.clusterserviceversion.yaml @@ -0,0 +1,19 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + capabilities: Basic Install + description: Creates and maintains an OpenShift Update Service instance + operatorframework.io/suggested-namespace: openshift-update-service + name: update-service-operator.v5.0.0 + namespace: placeholder +spec: + installModes: + - supported: true + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: false + type: AllNamespaces diff --git a/testdata/bundles/registry/invalid/metadata/annotations.yaml b/testdata/bundles/registry/invalid/metadata/annotations.yaml new file mode 100644 index 000000000..e1b2b9a5c --- /dev/null +++ b/testdata/bundles/registry/invalid/metadata/annotations.yaml @@ -0,0 +1,19 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: cincinnati-operator + operators.operatorframework.io.bundle.channels.v1: v1 + operators.operatorframework.io.bundle.channel.default.v1: v1 + operators.operatorframework.io.metrics.builder: operator-sdk-v1.9.0+git + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v3 + + # The following annotation will make your bundle be published on + # 4.8 and carried on to the upper versions 4.9, 4.10, etc. + com.redhat.openshift.versions: "v4.8" + + # Annotations for testing. + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 + operators.operatorframework.io.test.config.v1: tests/scorecard/