Skip to content

Commit 4038739

Browse files
michalvavriksquakez
authored andcommitted
feat(traits): gitops trait for Pipe
* Closes: #6421 Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>
1 parent fe84dbb commit 4038739

File tree

7 files changed

+202
-16
lines changed

7 files changed

+202
-16
lines changed

docs/modules/ROOT/pages/running/gitops.adoc

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[[gitops]]
22
= Camel GitOps
33

4-
Once your build is complete, you can configure the operator to run an opinionated GitOps strategy. Camel K has a built-in feature which allow the operator to push a branch on a given Git repository with the latest Integration candidate release built. In order to set the context, this would be the scenario:
4+
Once your build is complete, you can configure the operator to run an opinionated GitOps strategy. Camel K has a built-in feature which allow the operator to push a branch on a given Git repository with the latest Integration or Pipe candidate release built. In order to set the context, this would be the scenario:
55

66
1. The dev operator builds the application from Git source
77
2. The dev operator push the container image
@@ -43,7 +43,31 @@ spec:
4343

4444
NOTE: There are more options to configure on the `gitops` trait. Feel free to have a look and learn on the trait documentation page directly.
4545

46-
As soon as the build of the Integration is completed, the operator will prepare the commit with the overlays. The structure would be like the following directory tree:
46+
The same trait can be used on a Pipe:
47+
48+
```yaml
49+
apiVersion: camel.apache.org/v1
50+
kind: Pipe
51+
metadata:
52+
name: timer-to-log
53+
spec:
54+
source:
55+
uri: timer:foo
56+
sink:
57+
uri: log:bar
58+
traits:
59+
gitops:
60+
enabled: true
61+
url: https://github.com/my-org/my-camel-apps.git
62+
secret: my-gh-token
63+
overlays:
64+
- staging
65+
- production
66+
```
67+
68+
NOTE: When used with a Pipe, the `url` and `secret` trait parameters are required.
69+
70+
As soon as the build is completed, the operator will prepare the commit with the overlays. For an Integration, the structure would be like the following directory tree:
4771

4872
```bash
4973
/integrations/
@@ -70,7 +94,30 @@ As soon as the build of the Integration is completed, the operator will prepare
7094

7195
The above structure could be used directly with `kubectl` (eg, `kubectl apply -k /tmp/integrations/sample/overlays/production`) or any CICD capable of running a similar deployment strategy.
7296

73-
The important thing to notice is that the **base** Integration is adding the container image that we've just built and any other trait which is required for the application to run correctly (and without the need to be rebuilt) on another environment:
97+
For a Pipe, the structure is similar but uses `pipe.yaml` and `patch-pipe.yaml` instead:
98+
99+
```bash
100+
/pipes/
101+
├── all
102+
│   └── overlays
103+
│   ├── production
104+
│   │   └── kustomization.yaml
105+
│   └── staging
106+
│   └── kustomization.yaml
107+
└── timer-to-log
108+
├── base
109+
│   ├── pipe.yaml
110+
│   └── kustomization.yaml
111+
└── overlays
112+
├── production
113+
│   ├── kustomization.yaml
114+
│   └── patch-pipe.yaml
115+
└── staging
116+
├── kustomization.yaml
117+
└── patch-pipe.yaml
118+
```
119+
120+
The important thing to notice is that the **base** resource (Integration or Pipe) is adding the container image that we've just built and any other trait which is required for the application to run correctly (and without the need to be rebuilt) on another environment:
74121

75122
```yaml
76123
apiVersion: camel.apache.org/v1
@@ -184,7 +231,9 @@ NOTE: this is the approach suggested in https://developers.redhat.com/e-books/pa
184231

185232
=== Push to the same repository you've used to build
186233

187-
If you're building application from Git and you want to push the changes back to the same, then, you don't need to configure the Git repository for `gitops` trait. If nothing is specified, the trait will get the configuration from `.spec.git` Integration. This approach may be good when you want to have a single repository containing all aspects of an application. In this case we suggest to use a directory named `ci` or `cicd` as a convention to store your GitOps configuration.
234+
If you're building application from Git and you want to push the changes back to the same repository, then, you don't need to configure the Git repository for `gitops` trait. If nothing is specified, the trait will get the configuration from `.spec.git` Integration. This approach may be good when you want to have a single repository containing all aspects of an application. In this case we suggest to use a directory named `ci` or `cicd` as a convention to store your GitOps configuration.
235+
236+
NOTE: This does not apply to Pipes, which must always specify the `url` on the `gitops` trait.
188237

189238
=== Chain of GitOps environments
190239

docs/modules/ROOT/partials/apis/camel-k-crds.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7392,9 +7392,9 @@ The listeners in the format "port;protocol" (default, "8080;HTTP").
73927392
73937393
* <<#_camel_apache_org_v1_Traits, Traits>>
73947394
7395-
The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration built.
7395+
The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration or Pipe built.
73967396
If the trait is enabled but no pull configuration is provided, then, the operator will use the values stored in Integration `.spec.git` field used
7397-
to pull the project.
7397+
to pull the project. When used with a Pipe, the `url` and `secret` parameters are required as Pipes do not have a `.spec.git` fallback.
73987398
73997399
74007400
[cols="2,2a",options="header"]

docs/modules/traits/pages/gitops.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// Start of autogenerated code - DO NOT EDIT! (badges)
44
// End of autogenerated code - DO NOT EDIT! (badges)
55
// Start of autogenerated code - DO NOT EDIT! (description)
6-
The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration built.
6+
The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration or Pipe built.
77
If the trait is enabled but no pull configuration is provided, then, the operator will use the values stored in Integration `.spec.git` field used
8-
to pull the project.
8+
to pull the project. When used with a Pipe, the `url` and `secret` parameters are required as Pipes do not have a `.spec.git` fallback.
99

1010

1111
This trait is available in the following profiles: **Kubernetes, Knative, OpenShift**.

pkg/apis/camel/v1/trait/gitops.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ limitations under the License.
1717

1818
package trait
1919

20-
// The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration built.
20+
// The GitOps Trait is used to configure the repository where you want to push a GitOps Kustomize overlay configuration of the Integration or Pipe built.
2121
// If the trait is enabled but no pull configuration is provided, then, the operator will use the values stored in Integration `.spec.git` field used
22-
// to pull the project.
22+
// to pull the project. When used with a Pipe, the `url` and `secret` parameters are required as Pipes do not have a `.spec.git` fallback.
2323
//
2424
// +camel-k:trait=gitops.
2525
//

pkg/trait/gitops.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ package trait
2020
import (
2121
"context"
2222
"errors"
23+
"fmt"
2324
"os"
2425
"path/filepath"
26+
"strings"
2527
"time"
2628

2729
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
@@ -32,6 +34,7 @@ import (
3234
"github.com/go-git/go-git/v5/plumbing"
3335
corev1 "k8s.io/api/core/v1"
3436
"k8s.io/utils/ptr"
37+
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
3538

3639
git "github.com/go-git/go-git/v5"
3740
"github.com/go-git/go-git/v5/plumbing/object"
@@ -104,6 +107,15 @@ func (t *gitOpsTrait) pushGitOpsRepo(ctx context.Context, it *v1.Integration, to
104107
// to a new branch.
105108
func (t *gitOpsTrait) pushGitOpsItInGitRepo(ctx context.Context, it *v1.Integration, dir, token string) error {
106109
gitConf := t.gitConf(it)
110+
111+
pipe, err := t.findOwnerPipe(ctx, it)
112+
if err != nil {
113+
return err
114+
} else if pipe != nil && gitConf.URL == "" {
115+
// Pipes have no .spec.git fallback, so the URL must be provided via the trait
116+
return errors.New("gitops trait requires a git URL when used with a Pipe")
117+
}
118+
107119
// Clone repo
108120
repo, err := util.CloneGitProject(gitConf, dir, token)
109121
if err != nil {
@@ -146,11 +158,21 @@ func (t *gitOpsTrait) pushGitOpsItInGitRepo(ctx context.Context, it *v1.Integrat
146158
return err
147159
}
148160

149-
for _, overlay := range t.getOverlays() {
150-
destIntegration := util.EditIntegration(it, kit, overlay, "")
151-
err = util.AppendKustomizeIntegration(destIntegration, ciCdDir, t.getOverwriteOverlay())
152-
if err != nil {
153-
return err
161+
if pipe != nil {
162+
for _, overlay := range t.getOverlays() {
163+
destPipe := util.EditPipe(pipe, it, kit, overlay, "")
164+
err = util.AppendKustomizePipe(destPipe, ciCdDir, t.getOverwriteOverlay())
165+
if err != nil {
166+
return err
167+
}
168+
}
169+
} else {
170+
for _, overlay := range t.getOverlays() {
171+
destIntegration := util.EditIntegration(it, kit, overlay, "")
172+
err = util.AppendKustomizeIntegration(destIntegration, ciCdDir, t.getOverwriteOverlay())
173+
if err != nil {
174+
return err
175+
}
154176
}
155177
}
156178

@@ -191,16 +213,37 @@ func (t *gitOpsTrait) pushGitOpsItInGitRepo(ctx context.Context, it *v1.Integrat
191213
}
192214

193215
// Publish a condition to notify the change was pushed to the branch
216+
resourceKind := v1.IntegrationKind
217+
if pipe != nil {
218+
resourceKind = v1.PipeKind
219+
}
194220
it.Status.SetCondition(
195221
v1.IntegrationConditionType("GitPushed"),
196222
corev1.ConditionTrue,
197223
"PushedToGit",
198-
"Integration changes pushed to branch "+branchName,
224+
fmt.Sprintf("%s changes pushed to branch %s", resourceKind, branchName),
199225
)
200226

201227
return nil
202228
}
203229

230+
// findOwnerPipe checks if the Integration was created by a Pipe and returns it.
231+
func (t *gitOpsTrait) findOwnerPipe(ctx context.Context, it *v1.Integration) (*v1.Pipe, error) {
232+
for _, o := range it.OwnerReferences {
233+
if o.Kind == v1.PipeKind && strings.HasPrefix(o.APIVersion, v1.SchemeGroupVersion.Group) {
234+
pipe := &v1.Pipe{}
235+
key := ctrl.ObjectKey{Namespace: it.Namespace, Name: o.Name}
236+
if err := t.Client.Get(ctx, key, pipe); err != nil {
237+
return nil, fmt.Errorf("could not load Pipe %q: %w", o.Name, err)
238+
}
239+
240+
return pipe, nil
241+
}
242+
}
243+
244+
return nil, nil
245+
}
246+
204247
// gitConf returns the git repo configuration where to pull the project from. If no value is provided, then, it takes
205248
// the value coming from Integration git project (if specified).
206249
func (t *gitOpsTrait) gitConf(it *v1.Integration) v1.GitConfigSpec {

pkg/trait/gitops_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"time"
2727

2828
v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
29+
"github.com/apache/camel-k/v2/pkg/internal"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031

3132
"github.com/go-git/go-git/v5/plumbing/object"
@@ -187,3 +188,92 @@ func getRemoteURL(dirPath string) (string, error) {
187188

188189
return urls[0], nil
189190
}
191+
192+
func newPipeGitOpsTraitTestSetup(t *testing.T) (*gitOpsTrait, v1.Pipe, v1.Integration) {
193+
t.Helper()
194+
trait, _ := newGitOpsTrait().(*gitOpsTrait)
195+
trait.IntegrationDirectory = "integrations"
196+
197+
pipe := v1.NewPipe("default", "test-pipe")
198+
pipe.UID = "pipe-uid-123"
199+
pipe.Spec = v1.PipeSpec{
200+
Source: v1.Endpoint{URI: ptr.To("timer:foo")},
201+
Sink: v1.Endpoint{URI: ptr.To("log:bar")},
202+
}
203+
204+
it := v1.NewIntegration("default", "test-pipe")
205+
it.OwnerReferences = []metav1.OwnerReference{
206+
{
207+
APIVersion: v1.SchemeGroupVersion.String(),
208+
Kind: v1.PipeKind,
209+
Name: pipe.Name,
210+
UID: pipe.UID,
211+
},
212+
}
213+
now := metav1.Now().Rfc3339Copy()
214+
it.Status = v1.IntegrationStatus{
215+
Image: "my-pipe-img",
216+
BuildTimestamp: &now,
217+
}
218+
219+
fakeClient, err := internal.NewFakeClient(&pipe)
220+
require.NoError(t, err)
221+
trait.Client = fakeClient
222+
223+
return trait, pipe, it
224+
}
225+
226+
func TestGitOpsPushRepoPipe(t *testing.T) {
227+
trait, pipe, it := newPipeGitOpsTraitTestSetup(t)
228+
trait.Overlays = []string{"dev", "prod"}
229+
srcGitDir := t.TempDir()
230+
tmpGitDir := t.TempDir()
231+
err := initFakeGitRepo(srcGitDir)
232+
require.NoError(t, err)
233+
trait.URL = srcGitDir
234+
235+
err = trait.pushGitOpsItInGitRepo(context.TODO(), &it, tmpGitDir, "fake")
236+
require.NoError(t, err)
237+
238+
assert.Contains(t,
239+
it.Status.GetCondition(v1.IntegrationConditionType("GitPushed")).Message,
240+
"Pipe changes pushed to branch cicd/candidate-release",
241+
)
242+
243+
// Verify branch and commit
244+
lastCommitMessage, err := getLastCommitMessage(tmpGitDir)
245+
require.NoError(t, err)
246+
assert.Contains(t, lastCommitMessage, "feat(ci): build complete")
247+
branchName, err := getBranchNameFromDir(tmpGitDir)
248+
require.NoError(t, err)
249+
assert.Contains(t, branchName, "cicd/candidate-release")
250+
251+
// Verify pipe.yaml in base (not integration.yaml)
252+
_, err = os.Stat(filepath.Join(tmpGitDir, "integrations", pipe.Name, "base", "pipe.yaml"))
253+
require.NoError(t, err)
254+
_, err = os.Stat(filepath.Join(tmpGitDir, "integrations", pipe.Name, "base", "integration.yaml"))
255+
assert.True(t, os.IsNotExist(err), "integration.yaml should not exist for Pipe gitops")
256+
257+
// Verify overlay directories with patch-pipe.yaml
258+
for _, overlay := range []string{"dev", "prod"} {
259+
overlayDir := filepath.Join(tmpGitDir, "integrations", pipe.Name, "overlays", overlay)
260+
gitopsDir, err := os.Stat(overlayDir)
261+
require.NoError(t, err)
262+
assert.True(t, gitopsDir.IsDir())
263+
_, err = os.Stat(filepath.Join(overlayDir, "patch-pipe.yaml"))
264+
require.NoError(t, err, "patch-pipe.yaml should exist in overlay %s", overlay)
265+
// Verify "all" profile is generated
266+
allKust := filepath.Join(tmpGitDir, "integrations", "all", "overlays", overlay, "kustomization.yaml")
267+
_, err = os.Stat(allKust)
268+
require.NoError(t, err, "all profile kustomization.yaml should exist for overlay %s", overlay)
269+
}
270+
}
271+
272+
func TestGitOpsPipeRequiresURL(t *testing.T) {
273+
trait, _, it := newPipeGitOpsTraitTestSetup(t)
274+
// No URL configured, no it.Spec.Git fallback
275+
tmpGitDir := t.TempDir()
276+
err := trait.pushGitOpsItInGitRepo(context.TODO(), &it, tmpGitDir, "fake")
277+
require.Error(t, err)
278+
assert.Contains(t, err.Error(), "gitops trait requires a git URL when used with a Pipe")
279+
}

pkg/util/gitops/gitops.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ func EditPipe(kb *v1.Pipe, it *v1.Integration, kit *v1.IntegrationKit, toNamespa
163163
traits.Container = &traitv1.ContainerTrait{}
164164
}
165165
traits.Container.Image = contImage
166+
// We make sure not to propagate further the gitops trait
167+
// to avoid infinite loops. If the user wants to do a chain based
168+
// strategy, she can use the patch-pipe and continue the chain on purpose
169+
traits.GitOps = nil
166170
if kit != nil {
167171
// We must provide the classpath expected for the IntegrationKit. This is calculated dynamically and
168172
// would get lost when creating the non managed build Integration. For this reason

0 commit comments

Comments
 (0)