Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import external Camel applications #4942

Merged
merged 8 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/manager/operator-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
# Change to true to be able to create synthetic Integrations
- name: CAMEL_K_SYNTHETIC_INTEGRATIONS
value: "false"
livenessProbe:
httpGet:
path: /healthz
Expand Down
1 change: 1 addition & 0 deletions config/rbac/namespaced/operator-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ rules:
- camel.apache.org
resources:
- builds
- integrations
verbs:
- delete
- apiGroups:
Expand Down
3 changes: 2 additions & 1 deletion docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
** xref:running/dev-mode.adoc[Developer mode]
** xref:running/dry-run.adoc[Dry run]
** xref:running/runtime-version.adoc[Camel version]
** xref:running/camel-runtimes.adoc[Camel runtimes]
** xref:running/quarkus-native.adoc[Quarkus Native]
** xref:running/camel-runtimes.adoc[Camel runtimes]
** xref:running/import.adoc[Import existing Camel apps]
** xref:running/run-from-github.adoc[Run from GitHub]
** xref:running/promoting.adoc[Promote an Integration]
** xref:running/knative-sink.adoc[Knative Sinks]
44 changes: 44 additions & 0 deletions docs/modules/ROOT/pages/running/import.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
= Importing existing Camel applications

You may have already a Camel application running on your cluster. You may have created it via a manual deployment, a CICD or any other deployment mechanism you have in place. Since the Camel K operator is meant to operate any Camel application out there, then, you will be able to import it and monitor in a similar fashion of any other Camel K **managed Integration**.

This feature is disabled by default. In order to enable it, you need to run the operator deployment with an environment variable, `CAMEL_K_SYNTHETIC_INTEGRATIONS`, set to `true`.

NOTE: you will be only able to monitor the synthetic Integrations. Camel K won't be able to alter the lifecycle of non managed Integrations (ie, rebuild the original application).

It's important to notice that the operator won't be altering any field of the original application in order to avoid breaking any deployment procedure which is already in place. As it cannot make any assumption on the way the application is built and deployed, it will only be able to **watch** for any changes happening around it.

[[deploy-and-monitor]]
== Deploy externally, monitor via Camel K Operator

An imported Integration is known as **synthetic Integration**. You can import any Camel application deployed as a **Deployment**, **CronJob** or **Knative Service**. We control this behavior via a label (`camel.apache.org/integration`) that the user need to apply on the Camel application (either manually or introducing in the deployment process, ie, via CICD).

NOTE: the example here will work in a similar way using CronJob and Knative Service.

As an example, we show how to import a Camel application which was deployed with the Deployment kind. Let's assume it is called `my-deploy`.
```
$ kubectl label deploy my-camel-sb-svc camel.apache.org/integration=my-it
```
The operator immediately creates a synthetic Integration:
```
$ kubectl get it
NAMESPACE NAME PHASE RUNTIME PROVIDER RUNTIME VERSION KIT REPLICAS
test-79c385c3-d58e-4c28-826d-b14b6245f908 my-it Running
```
You can see it will be in `Running` status phase. However, checking the conditions you will be able to see that the Integration is not yet able to be fully monitored. This is expected because the way Camel K operator monitor Pods. It requires that the same label applied to the Deployment is inherited by the generated Pods. For this reason, beside labelling the Deployment, we need to add a label in the Deployment template.
```
$ kubectl patch deployment my-camel-sb-svc --patch '{"spec": {"template": {"metadata": {"labels": {"camel.apache.org/integration": "my-it"}}}}}'
```
Also this operation can be performed manually or automated in the deployment procedure. We can see now that the operator will be able to monitor accordingly the status of the Pods:
```
$ kubectl get it
NAMESPACE NAME PHASE RUNTIME PROVIDER RUNTIME VERSION KIT REPLICAS
test-79c385c3-d58e-4c28-826d-b14b6245f908 my-it Running 1
```
From now on, you will be able to monitor the status of the synthetic Integration in a similar fashion of what you do with managed Integrations. If, for example, your Deployment will scale up or down, then, you will see this information reflecting accordingly:
```
$ kubectl scale deployment my-camel-sb-svc --replicas 2
$ kubectl get it
NAMESPACE NAME PHASE RUNTIME PROVIDER RUNTIME VERSION KIT REPLICAS
test-79c385c3-d58e-4c28-826d-b14b6245f908 my-it Running 2
```
50 changes: 50 additions & 0 deletions e2e/commonwithcustominstall/files/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
apiVersion: v1
data:
my-file.txt: hello
kind: ConfigMap
metadata:
name: my-cm
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: my-camel-sb-svc
name: my-camel-sb-svc
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: my-camel-sb-svc
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: my-camel-sb-svc
spec:
containers:
- image: docker.io/squakez/my-camel-sb-svc:1.0.0
imagePullPolicy: IfNotPresent
name: my-camel-sb-svc
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- name: my-cm
mountPath: /tmp/app/data
volumes:
- name: my-cm
configMap:
name: my-cm
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
119 changes: 119 additions & 0 deletions e2e/commonwithcustominstall/synthetic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//go:build integration
// +build integration

// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"

/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package commonwithcustominstall

import (
"testing"

. "github.com/onsi/gomega"

. "github.com/apache/camel-k/v2/e2e/support"
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
. "github.com/onsi/gomega/gstruct"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
)

func TestSyntheticIntegrationOff(t *testing.T) {
RegisterTestingT(t)
WithNewTestNamespace(t, func(ns string) {
// Install Camel K without synthetic Integration feature variable (default)
operatorID := "camel-k-synthetic-env-off"
Expect(KamelInstallWithID(operatorID, ns).Execute()).To(Succeed())

// Run the external deployment
ExpectExecSucceed(t, Kubectl("apply", "-f", "files/deploy.yaml", "-n", ns))
Eventually(DeploymentCondition(ns, "my-camel-sb-svc", appsv1.DeploymentProgressing), TestTimeoutShort).
Should(MatchFields(IgnoreExtras, Fields{
"Status": Equal(corev1.ConditionTrue),
"Reason": Equal("NewReplicaSetAvailable"),
}))

// Label the deployment --> Verify the Integration is not created
ExpectExecSucceed(t, Kubectl("label", "deploy", "my-camel-sb-svc", "camel.apache.org/integration=my-it", "-n", ns))
Eventually(Integration(ns, "my-it"), TestTimeoutShort).Should(BeNil())
})
}
func TestSyntheticIntegrationFromDeployment(t *testing.T) {
RegisterTestingT(t)
WithNewTestNamespace(t, func(ns string) {
// Install Camel K with the synthetic Integration feature variable
operatorID := "camel-k-synthetic-env"
Expect(KamelInstallWithID(operatorID, ns,
"--operator-env-vars", "CAMEL_K_SYNTHETIC_INTEGRATIONS=true",
).Execute()).To(Succeed())

// Run the external deployment
ExpectExecSucceed(t, Kubectl("apply", "-f", "files/deploy.yaml", "-n", ns))
Eventually(DeploymentCondition(ns, "my-camel-sb-svc", appsv1.DeploymentProgressing), TestTimeoutShort).
Should(MatchFields(IgnoreExtras, Fields{
"Status": Equal(corev1.ConditionTrue),
"Reason": Equal("NewReplicaSetAvailable"),
}))

// Label the deployment --> Verify the Integration is created (cannot still monitor)
ExpectExecSucceed(t, Kubectl("label", "deploy", "my-camel-sb-svc", "camel.apache.org/integration=my-it", "-n", ns))
Eventually(IntegrationPhase(ns, "my-it"), TestTimeoutShort).Should(Equal(v1.IntegrationPhaseRunning))
Eventually(IntegrationConditionStatus(ns, "my-it", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionFalse))
Eventually(IntegrationCondition(ns, "my-it", v1.IntegrationConditionReady), TestTimeoutShort).Should(
WithTransform(IntegrationConditionReason, Equal(v1.IntegrationConditionMonitoringPodsAvailableReason)))

// Label the deployment template --> Verify the Integration is monitored
ExpectExecSucceed(t, Kubectl("patch", "deployment", "my-camel-sb-svc", "--patch", `{"spec": {"template": {"metadata": {"labels": {"camel.apache.org/integration": "my-it"}}}}}`, "-n", ns))
Eventually(IntegrationPhase(ns, "my-it"), TestTimeoutShort).Should(Equal(v1.IntegrationPhaseRunning))
Eventually(IntegrationConditionStatus(ns, "my-it", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
one := int32(1)
Eventually(IntegrationStatusReplicas(ns, "my-it"), TestTimeoutShort).Should(Equal(&one))

// Delete the deployment --> Verify the Integration is eventually garbage collected
ExpectExecSucceed(t, Kubectl("delete", "deploy", "my-camel-sb-svc", "-n", ns))
Eventually(Integration(ns, "my-it"), TestTimeoutShort).Should(BeNil())

// Recreate the deployment and label --> Verify the Integration is monitored
ExpectExecSucceed(t, Kubectl("apply", "-f", "files/deploy.yaml", "-n", ns))
ExpectExecSucceed(t, Kubectl("label", "deploy", "my-camel-sb-svc", "camel.apache.org/integration=my-it", "-n", ns))
ExpectExecSucceed(t, Kubectl("patch", "deployment", "my-camel-sb-svc", "--patch", `{"spec": {"template": {"metadata": {"labels": {"camel.apache.org/integration": "my-it"}}}}}`, "-n", ns))
Eventually(IntegrationPhase(ns, "my-it"), TestTimeoutShort).Should(Equal(v1.IntegrationPhaseRunning))
Eventually(IntegrationConditionStatus(ns, "my-it", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
Eventually(IntegrationStatusReplicas(ns, "my-it"), TestTimeoutShort).Should(Equal(&one))

// Remove label from the deployment --> Verify the Integration is deleted
ExpectExecSucceed(t, Kubectl("label", "deploy", "my-camel-sb-svc", "camel.apache.org/integration-", "-n", ns))
Eventually(Integration(ns, "my-it"), TestTimeoutShort).Should(BeNil())

// Add label back to the deployment --> Verify the Integration is created
ExpectExecSucceed(t, Kubectl("label", "deploy", "my-camel-sb-svc", "camel.apache.org/integration=my-it", "-n", ns))
Eventually(IntegrationPhase(ns, "my-it"), TestTimeoutShort).Should(Equal(v1.IntegrationPhaseRunning))
Eventually(IntegrationConditionStatus(ns, "my-it", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
Eventually(IntegrationStatusReplicas(ns, "my-it"), TestTimeoutShort).Should(Equal(&one))
// Scale the deployment --> verify replicas are correctly set
ExpectExecSucceed(t, Kubectl("scale", "deploy", "my-camel-sb-svc", "--replicas", "2", "-n", ns))
two := int32(2)
Eventually(IntegrationStatusReplicas(ns, "my-it"), TestTimeoutShort).Should(Equal(&two))

// Delete Integration and deployments --> verify no Integration exists any longer
Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
ExpectExecSucceed(t, Kubectl("delete", "deploy", "my-camel-sb-svc", "-n", ns))
Eventually(Integration(ns, "my-it"), TestTimeoutShort).Should(BeNil())
})
}
8 changes: 8 additions & 0 deletions e2e/support/test_support.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,14 @@ func MakeWithContext(ctx context.Context, rule string, args ...string) *exec.Cmd
return exec.Command("make", args...)
}

func Kubectl(args ...string) *exec.Cmd {
return KubectlWithContext(TestContext, args...)
}

func KubectlWithContext(ctx context.Context, args ...string) *exec.Cmd {
return exec.Command("kubectl", args...)
}

// =============================================================================
// Curried utility functions for testing
// =============================================================================
Expand Down
1 change: 1 addition & 0 deletions helm/camel-k/templates/operator-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ rules:
- camel.apache.org
resources:
- builds
- integrations
verbs:
- delete
- apiGroups:
Expand Down
12 changes: 7 additions & 5 deletions pkg/apis/camel/v1/integration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ const (
// IntegrationPhaseError --.
IntegrationPhaseError IntegrationPhase = "Error"

// IntegrationConditionReady --.
IntegrationConditionReady IntegrationConditionType = "Ready"
// IntegrationConditionKitAvailable --.
IntegrationConditionKitAvailable IntegrationConditionType = "IntegrationKitAvailable"
// IntegrationConditionPlatformAvailable --.
Expand All @@ -178,10 +180,9 @@ const (
IntegrationConditionJolokiaAvailable IntegrationConditionType = "JolokiaAvailable"
// IntegrationConditionProbesAvailable --.
IntegrationConditionProbesAvailable IntegrationConditionType = "ProbesAvailable"
// IntegrationConditionReady --.
IntegrationConditionReady IntegrationConditionType = "Ready"
// IntegrationConditionTraitInfo --.
IntegrationConditionTraitInfo IntegrationConditionType = "TraitInfo"

// IntegrationConditionKitAvailableReason --.
IntegrationConditionKitAvailableReason string = "IntegrationKitAvailable"
// IntegrationConditionPlatformAvailableReason --.
Expand Down Expand Up @@ -220,7 +221,8 @@ const (
IntegrationConditionJolokiaAvailableReason string = "JolokiaAvailable"
// IntegrationConditionProbesAvailableReason --.
IntegrationConditionProbesAvailableReason string = "ProbesAvailable"

// IntegrationConditionMonitoringPodsAvailableReason used to specify that the Pods generated are available for monitoring.
IntegrationConditionMonitoringPodsAvailableReason string = "MonitoringPodsAvailable"
squakez marked this conversation as resolved.
Show resolved Hide resolved
// IntegrationConditionKnativeServiceReadyReason --.
IntegrationConditionKnativeServiceReadyReason string = "KnativeServiceReady"
// IntegrationConditionDeploymentReadyReason --.
Expand All @@ -239,18 +241,18 @@ const (
IntegrationConditionRuntimeNotReadyReason string = "RuntimeNotReady"
// IntegrationConditionErrorReason --.
IntegrationConditionErrorReason string = "Error"

// IntegrationConditionInitializationFailedReason --.
IntegrationConditionInitializationFailedReason string = "InitializationFailed"
// IntegrationConditionUnsupportedLanguageReason --.
IntegrationConditionUnsupportedLanguageReason string = "UnsupportedLanguage"

// IntegrationConditionKameletsAvailable --.
IntegrationConditionKameletsAvailable IntegrationConditionType = "KameletsAvailable"
// IntegrationConditionKameletsAvailableReason --.
IntegrationConditionKameletsAvailableReason string = "KameletsAvailable"
// IntegrationConditionKameletsNotAvailableReason --.
IntegrationConditionKameletsNotAvailableReason string = "KameletsNotAvailable"
// IntegrationConditionImportingKindAvailableReason used (as false) if we're trying to import an unsupported kind.
IntegrationConditionImportingKindAvailableReason string = "ImportingKindAvailable"
)

// IntegrationCondition describes the state of a resource at a certain point.
Expand Down
15 changes: 15 additions & 0 deletions pkg/apis/camel/v1/integration_types_support.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,18 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// IntegrationLabel is used to tag k8s object created by a given Integration.
const IntegrationLabel = "camel.apache.org/integration"

// IntegrationSyntheticLabel is used to tag k8s synthetic Integrations.
const IntegrationSyntheticLabel = "camel.apache.org/is-synthetic"

// IntegrationImportedKindLabel specifies from what kind of resource an Integration was imported.
const IntegrationImportedKindLabel = "camel.apache.org/imported-from-kind"
squakez marked this conversation as resolved.
Show resolved Hide resolved

// IntegrationImportedNameLabel specifies from what resource an Integration was imported.
const IntegrationImportedNameLabel = "camel.apache.org/imported-from-name"

func NewIntegration(namespace string, name string) Integration {
return Integration{
TypeMeta: metav1.TypeMeta{
Expand Down Expand Up @@ -283,6 +293,11 @@ func (in *Integration) SetReadyConditionError(err string) {
in.SetReadyCondition(corev1.ConditionFalse, IntegrationConditionErrorReason, err)
}

// IsSynthetic returns true for synthetic Integrations (non managed, likely imported from external deployments).
func (in *Integration) IsSynthetic() bool {
return in.Annotations[IntegrationSyntheticLabel] == "true"
}

// GetCondition returns the condition with the provided type.
func (in *IntegrationStatus) GetCondition(condType IntegrationConditionType) *IntegrationCondition {
for i := range in.Conditions {
Expand Down
8 changes: 8 additions & 0 deletions pkg/cmd/operator/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import (
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
"github.com/apache/camel-k/v2/pkg/client"
"github.com/apache/camel-k/v2/pkg/controller"
"github.com/apache/camel-k/v2/pkg/controller/synthetic"
"github.com/apache/camel-k/v2/pkg/event"
"github.com/apache/camel-k/v2/pkg/install"
"github.com/apache/camel-k/v2/pkg/platform"
Expand Down Expand Up @@ -231,6 +232,13 @@ func Run(healthPort, monitoringPort int32, leaderElection bool, leaderElectionID
install.OperatorStartupOptionalTools(installCtx, bootstrapClient, watchNamespace, operatorNamespace, log)
exitOnError(findOrCreateIntegrationPlatform(installCtx, bootstrapClient, operatorNamespace), "failed to create integration platform")

synthEnvVal, synth := os.LookupEnv("CAMEL_K_SYNTHETIC_INTEGRATIONS")
if synth && synthEnvVal == "true" {
log.Info("Starting the synthetic Integration manager")
exitOnError(synthetic.ManageSyntheticIntegrations(ctx, ctrlClient, mgr.GetCache()), "synthetic Integration manager error")
} else {
log.Info("Synthetic Integration manager not configured, skipping")
}
log.Info("Starting the manager")
exitOnError(mgr.Start(ctx), "manager exited non-zero")
}
Expand Down
Loading
Loading