You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by nf...@apache.org on 2021/03/03 12:56:34 UTC
[camel-k] 02/08: Fix #1799: added system:image-puller delegation
for OpenShift
This is an automated email from the ASF dual-hosted git repository.
nferraro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 7552e0305ccfd4c1e3bd355373d0ec770834b4d7
Author: nicolaferraro <ni...@gmail.com>
AuthorDate: Fri Feb 19 12:21:42 2021 +0100
Fix #1799: added system:image-puller delegation for OpenShift
---
deploy/traits.yaml | 3 +
.../attachments/schema/integration-schema.json | 3 +
docs/modules/traits/pages/pull-secret.adoc | 4 ++
.../integration/integration_controller.go | 14 ++--
pkg/platform/defaults.go | 13 +++-
pkg/trait/cron_test.go | 74 ---------------------
pkg/trait/pull_secret.go | 75 ++++++++++++++++++++--
pkg/util/test/client.go | 23 +++++++
8 files changed, 124 insertions(+), 85 deletions(-)
diff --git a/deploy/traits.yaml b/deploy/traits.yaml
index afde044..bbf8d9a 100755
--- a/deploy/traits.yaml
+++ b/deploy/traits.yaml
@@ -533,6 +533,9 @@ traits:
- name: secret-name
type: string
description: The pull secret name to set on the Pod. If left empty this is automatically taken from the `IntegrationPlatform` registry configuration.
+ - name: image-puller-delegation
+ type: bool
+ description: When using a global operator with a shared platform, this enables delegation of the `system:image-puller` cluster role on the operator namespace to the integration service account.
- name: auto
type: bool
description: Automatically configures the platform registry secret on the pod if it is of type `kubernetes.io/dockerconfigjson`.
diff --git a/docs/modules/ROOT/assets/attachments/schema/integration-schema.json b/docs/modules/ROOT/assets/attachments/schema/integration-schema.json
index 249e3f3..7423a3c 100644
--- a/docs/modules/ROOT/assets/attachments/schema/integration-schema.json
+++ b/docs/modules/ROOT/assets/attachments/schema/integration-schema.json
@@ -433,6 +433,9 @@
"platform": {
"type": "string"
},
+ "platformNamespace": {
+ "type": "string"
+ },
"profile": {
"description": "TraitProfile represents lists of traits that are enabled for the specific installation/integration",
"type": "string"
diff --git a/docs/modules/traits/pages/pull-secret.adoc b/docs/modules/traits/pages/pull-secret.adoc
index 0815ae4..8d8fee2 100755
--- a/docs/modules/traits/pages/pull-secret.adoc
+++ b/docs/modules/traits/pages/pull-secret.adoc
@@ -38,6 +38,10 @@ The following configuration options are available:
| string
| The pull secret name to set on the Pod. If left empty this is automatically taken from the `IntegrationPlatform` registry configuration.
+| pull-secret.image-puller-delegation
+| bool
+| When using a global operator with a shared platform, this enables delegation of the `system:image-puller` cluster role on the operator namespace to the integration service account.
+
| pull-secret.auto
| bool
| Automatically configures the platform registry secret on the pod if it is of type `kubernetes.io/dockerconfigjson`.
diff --git a/pkg/controller/integration/integration_controller.go b/pkg/controller/integration/integration_controller.go
index 2e1362b..abe0d90 100644
--- a/pkg/controller/integration/integration_controller.go
+++ b/pkg/controller/integration/integration_controller.go
@@ -150,20 +150,26 @@ func add(mgr manager.Manager, r reconcile.Reconciler, c client.Client) error {
// requests for any integrations that are in phase waiting for platform
err = ctrl.Watch(&source.Kind{Type: &v1.IntegrationPlatform{}}, &handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request {
- platform := a.Object.(*v1.IntegrationPlatform)
+ pl := a.Object.(*v1.IntegrationPlatform)
var requests []reconcile.Request
- if platform.Status.Phase == v1.IntegrationPlatformPhaseReady {
+ if pl.Status.Phase == v1.IntegrationPlatformPhaseReady {
list := &v1.IntegrationList{}
- if err := mgr.GetClient().List(context.TODO(), list, k8sclient.InNamespace(platform.Namespace)); err != nil {
+ // Do global search in case of global operator (it may be using a global platform)
+ var opts []k8sclient.ListOption
+ if !platform.IsCurrentOperatorGlobal() {
+ opts = append(opts, k8sclient.InNamespace(pl.Namespace))
+ }
+
+ if err := mgr.GetClient().List(context.TODO(), list, opts...); err != nil {
log.Error(err, "Failed to retrieve integration list")
return requests
}
for _, integration := range list.Items {
if integration.Status.Phase == v1.IntegrationPhaseWaitingForPlatform {
- log.Infof("Platform %s ready, wake-up integration: %s", platform.Name, integration.Name)
+ log.Infof("Platform %s ready, wake-up integration: %s", pl.Name, integration.Name)
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: integration.Namespace,
diff --git a/pkg/platform/defaults.go b/pkg/platform/defaults.go
index 9da4e88..8b86acc 100644
--- a/pkg/platform/defaults.go
+++ b/pkg/platform/defaults.go
@@ -76,14 +76,23 @@ func ConfigureDefaults(ctx context.Context, c client.Client, p *v1.IntegrationPl
}
if p.Status.Build.BuildStrategy == "" {
- // If the operator is global, a global build strategy should be used
- if IsCurrentOperatorGlobal() {
+ operatorIsLocal := !IsCurrentOperatorGlobal()
+ // Local operators build in the same namespace by definition
+ buildInDifferentNamespace := !operatorIsLocal
+ if !operatorIsLocal {
+ // Unless it's a global operator using a globally shared integration platform
+ buildInDifferentNamespace = p.Namespace != GetOperatorNamespace()
+ }
+
+ // If the operator and the build are in different namespaces, pod strategy should be used (except for Spectrum)
+ if buildInDifferentNamespace {
if p.Status.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategySpectrum {
p.Status.Build.BuildStrategy = v1.IntegrationPlatformBuildStrategyRoutine
} else {
p.Status.Build.BuildStrategy = v1.IntegrationPlatformBuildStrategyPod
}
} else {
+ // Same-namespace builds use the fastest strategy that they support (routine when possible)
if p.Status.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategyS2I ||
p.Status.Build.PublishStrategy == v1.IntegrationPlatformBuildPublishStrategySpectrum {
p.Status.Build.BuildStrategy = v1.IntegrationPlatformBuildStrategyRoutine
diff --git a/pkg/trait/cron_test.go b/pkg/trait/cron_test.go
index 07f77fa..f445cc3 100644
--- a/pkg/trait/cron_test.go
+++ b/pkg/trait/cron_test.go
@@ -365,80 +365,6 @@ func TestCronDepsFallback(t *testing.T) {
assert.Contains(t, environment.Integration.Status.Dependencies, "mvn:org.apache.camel.k:camel-k-cron")
}
-func TestCronWithMain(t *testing.T) {
- catalog, err := camel.DefaultCatalog()
- assert.Nil(t, err)
-
- traitCatalog := NewCatalog(context.TODO(), nil)
-
- environment := Environment{
- CamelCatalog: catalog,
- Catalog: traitCatalog,
- Integration: &v1.Integration{
- ObjectMeta: metav1.ObjectMeta{
- Name: "test",
- Namespace: "ns",
- },
- Status: v1.IntegrationStatus{
- Phase: v1.IntegrationPhaseDeploying,
- },
- Spec: v1.IntegrationSpec{
- Profile: v1.TraitProfileKnative,
- Sources: []v1.SourceSpec{
- {
- DataSpec: v1.DataSpec{
- Name: "routes.java",
- Content: `from("cron:tab?schedule=0 0/2 * * ?").to("log:test")`,
- },
- Language: v1.LanguageJavaSource,
- },
- },
- Resources: []v1.ResourceSpec{},
- Traits: map[string]v1.TraitSpec{
- "quarkus": test.TraitSpecFromMap(t, map[string]interface{}{
- "enabled": false,
- }),
- },
- },
- },
- IntegrationKit: &v1.IntegrationKit{
- Status: v1.IntegrationKitStatus{
- Phase: v1.IntegrationKitPhaseReady,
- },
- },
- Platform: &v1.IntegrationPlatform{
- Spec: v1.IntegrationPlatformSpec{
- Cluster: v1.IntegrationPlatformClusterOpenShift,
- Build: v1.IntegrationPlatformBuildSpec{
- PublishStrategy: v1.IntegrationPlatformBuildPublishStrategyS2I,
- Registry: v1.IntegrationPlatformRegistrySpec{Address: "registry"},
- },
- Profile: v1.TraitProfileKnative,
- },
- },
- EnvVars: make([]corev1.EnvVar, 0),
- ExecutedTraits: make([]Trait, 0),
- Resources: k8sutils.NewCollection(),
- }
- environment.Platform.ResyncStatusFullConfig()
-
- c, err := NewFakeClient("ns")
- assert.Nil(t, err)
-
- tc := NewCatalog(context.TODO(), c)
-
- err = tc.apply(&environment)
-
- assert.Nil(t, err)
- assert.NotEmpty(t, environment.ExecutedTraits)
- assert.Nil(t, environment.GetTrait("quarkus"))
-
- ct := environment.GetTrait("cron").(*cronTrait)
- assert.NotNil(t, ct)
- assert.Nil(t, ct.Fallback)
- assert.Contains(t, environment.Interceptors, "cron")
-}
-
func TestCronWithQuarkus(t *testing.T) {
catalog, err := camel.DefaultCatalog()
assert.Nil(t, err)
diff --git a/pkg/trait/pull_secret.go b/pkg/trait/pull_secret.go
index 9bb6b19..1c19af7 100644
--- a/pkg/trait/pull_secret.go
+++ b/pkg/trait/pull_secret.go
@@ -18,9 +18,17 @@ limitations under the License.
package trait
import (
+ "fmt"
+
v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+ "github.com/apache/camel-k/pkg/platform"
"github.com/apache/camel-k/pkg/util"
+ "github.com/apache/camel-k/pkg/util/kubernetes"
+ "github.com/apache/camel-k/pkg/util/openshift"
+ "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -40,6 +48,8 @@ type pullSecretTrait struct {
BaseTrait `property:",squash"`
// The pull secret name to set on the Pod. If left empty this is automatically taken from the `IntegrationPlatform` registry configuration.
SecretName string `property:"secret-name" json:"secretName,omitempty"`
+ // When using a global operator with a shared platform, this enables delegation of the `system:image-puller` cluster role on the operator namespace to the integration service account.
+ ImagePullerDelegation *bool `property:"image-puller-delegation" json:"imagePullerDelegation,omitempty"`
// Automatically configures the platform registry secret on the pod if it is of type `kubernetes.io/dockerconfigjson`.
Auto *bool `property:"auto" json:"auto,omitempty"`
}
@@ -73,17 +83,72 @@ func (t *pullSecretTrait) Configure(e *Environment) (bool, error) {
}
}
}
+ if t.ImagePullerDelegation == nil {
+ var isOpenshift bool
+ if t.Client != nil {
+ var err error
+ isOpenshift, err = openshift.IsOpenShift(t.Client)
+ if err != nil {
+ return false, err
+ }
+ }
+ isOperatorGlobal := platform.IsCurrentOperatorGlobal()
+ isKitExternal := e.Integration.GetIntegrationKitNamespace() != e.Integration.Namespace
+ needsDelegation := isOpenshift && isOperatorGlobal && isKitExternal
+ t.ImagePullerDelegation = &needsDelegation
+ }
}
- return t.SecretName != "", nil
+ return t.SecretName != "" || util.IsTrue(t.ImagePullerDelegation), nil
}
func (t *pullSecretTrait) Apply(e *Environment) error {
- e.Resources.VisitPodSpec(func(p *corev1.PodSpec) {
- p.ImagePullSecrets = append(p.ImagePullSecrets, corev1.LocalObjectReference{
- Name: t.SecretName,
+ if t.SecretName != "" {
+ e.Resources.VisitPodSpec(func(p *corev1.PodSpec) {
+ p.ImagePullSecrets = append(p.ImagePullSecrets, corev1.LocalObjectReference{
+ Name: t.SecretName,
+ })
})
- })
+ }
+ if util.IsTrue(t.ImagePullerDelegation) {
+ if err := t.delegateImagePuller(e); err != nil {
+ return err
+ }
+ }
return nil
}
+
+func (t *pullSecretTrait) delegateImagePuller(e *Environment) error {
+ // Applying the rolebinding directly because it's a resource in the operator namespace
+ // (different from the integration namespace when delegation is enabled).
+ rb := t.newImagePullerRoleBinding(e)
+ if err := kubernetes.ReplaceResource(e.C, e.Client, rb); err != nil {
+ return errors.Wrap(err, "error during the creation of the system:image-puller delegating role binding")
+ }
+ return nil
+}
+
+func (t *pullSecretTrait) newImagePullerRoleBinding(e *Environment) *rbacv1.RoleBinding {
+ serviceAccount := e.Integration.Spec.ServiceAccountName
+ if serviceAccount == "" {
+ serviceAccount = "default"
+ }
+ return &rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: e.Integration.GetIntegrationKitNamespace(),
+ Name: fmt.Sprintf("camel-k-puller-%s", e.Integration.Namespace),
+ },
+ RoleRef: rbacv1.RoleRef{
+ Kind: "ClusterRole",
+ Name: "system:image-puller",
+ },
+ Subjects: []rbacv1.Subject{
+ {
+ Kind: "ServiceAccount",
+ Namespace: e.Integration.Namespace,
+ Name: serviceAccount,
+ },
+ },
+ }
+}
diff --git a/pkg/util/test/client.go b/pkg/util/test/client.go
index 0657673..69c8f05 100644
--- a/pkg/util/test/client.go
+++ b/pkg/util/test/client.go
@@ -26,8 +26,11 @@ import (
fakecamelclientset "github.com/apache/camel-k/pkg/client/camel/clientset/versioned/fake"
camelv1 "github.com/apache/camel-k/pkg/client/camel/clientset/versioned/typed/camel/v1"
camelv1alpha1 "github.com/apache/camel-k/pkg/client/camel/clientset/versioned/typed/camel/v1alpha1"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
fakeclientset "k8s.io/client-go/kubernetes/fake"
clientscheme "k8s.io/client-go/kubernetes/scheme"
@@ -101,3 +104,23 @@ func (c *FakeClient) GetConfig() *rest.Config {
func (c *FakeClient) GetCurrentNamespace(kubeConfig string) (string, error) {
return "", nil
}
+
+func (c *FakeClient) Discovery() discovery.DiscoveryInterface {
+ return &FakeDiscovery{
+ DiscoveryInterface: c.Interface.Discovery(),
+ }
+}
+
+type FakeDiscovery struct {
+ discovery.DiscoveryInterface
+}
+
+func (f *FakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
+ // Normalize the fake discovery to behave like the real implementation when checking for openshift
+ if groupVersion == "image.openshift.io/v1" {
+ return nil, k8serrors.NewNotFound(schema.GroupResource{
+ Group: "image.openshift.io",
+ }, "")
+ }
+ return f.DiscoveryInterface.ServerResourcesForGroupVersion(groupVersion)
+}