Skip to content

Commit 6be06b8

Browse files
Merge branch 'liqotech:master' into unpeering_force
2 parents bef40d6 + 2b41212 commit 6be06b8

File tree

5 files changed

+290
-17
lines changed

5 files changed

+290
-17
lines changed

docs/conf.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,33 @@ def generate_version() -> str:
167167
version = x.json()['tag_name']
168168
return version
169169

170+
def generate_argocd_manifest() -> str:
171+
return f"""```yaml
172+
apiVersion: argoproj.io/v1alpha1
173+
kind: Application
174+
metadata:
175+
name: liqo-app
176+
namespace: argocd
177+
spec:
178+
project: default # Place here your project name
179+
source:
180+
chart: liqo
181+
repoURL: https://helm.liqo.io/
182+
targetRevision: {generate_version()}
183+
helm:
184+
releaseName: liqo
185+
values: |
186+
HERE_YOUR_VALUES_FILE
187+
destination:
188+
server: "https://kubernetes.default.svc" # Place here your destination cluster
189+
namespace: liqo
190+
syncPolicy:
191+
syncOptions:
192+
- CreateNamespace=true
193+
- ServerSideApply=true
194+
```
195+
"""
196+
170197
# generate_link_to_repo generates a link to the repository for the given file, according to the current version of the documentation.
171198
def generate_link_to_repo(text: str, file_path: str) -> str:
172199
version = generate_version()
@@ -237,6 +264,7 @@ def generate_liqoctl_version_warning() -> str:
237264
html_context = {
238265
'generate_clone_example': generate_clone_example,
239266
'generate_clone_example_tf': generate_clone_example_tf,
267+
'generate_argocd_manifest': generate_argocd_manifest,
240268
'generate_liqoctl_install': generate_liqoctl_install,
241269
'generate_link_to_repo': generate_link_to_repo,
242270
'generate_helm_install': generate_helm_install,

docs/installation/install.md

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -543,26 +543,72 @@ liqoctl info --get network.podcidr
543543

544544
## ArgoCD
545545

546-
### Annotations
546+
[ArgoCD](https://argo-cd.readthedocs.io/en/stable/) is a GitOps continuous delivery tool designed for Kubernetes.
547+
**Liqo supports installation and configuration with ArgoCD** via a declarative approach.
547548

548-
When using ArgoCD to install Liqo, we recommend to set the following values:
549+
The following is an example of ArgoCD application installing the [Liqo Helm chart](#install-with-helm).
549550

550-
```yaml
551-
common:
552-
globalAnnotations:
553-
argocd.argoproj.io/ignore-resource-updates: "true"
554-
argocd.argoproj.io/sync-options: Delete=true
555-
argocd.argoproj.io/sync-wave: 10
556-
argocd.argoproj.io/compare-options: IgnoreExtraneous
551+
{{ env.config.html_context.generate_argocd_manifest() }}
552+
553+
In the previous example, you should define the values to configure your Liqo installation.
554+
You can generate a template of the `spec.source.helm.values` field via `liqoctl`:
555+
556+
```bash
557+
liqoctl install <provider> [flags] --only-output-values
557558
```
558559

559-
This instruments Liqo to add some **argocd annotations** to the resources it creates.
560-
These annotations allow to:
560+
The previous command produces a pre-configured `values.yaml` file in the working directory, which you can customize and place in the `spec.source.helm.values` field of the ArgoCD application manifest.
561+
562+
Check the [ArgoCD documentation](https://argo-cd.readthedocs.io/en/latest/user-guide/helm/) for further info about how to install a Helm Chart via ArgoCD.
563+
564+
### Ensure clean Liqo removal with ArgoCD
565+
566+
When Liqo is installed via ArgoCD, to ensure the clean removal of all the Liqo resources during the uninstallation process, some configurations need to be applied to the ArgoCD `Application` manifest.
567+
568+
Depending on the version of ArgoCD you are running or the enabled [resource tracking strategy](https://argo-cd.readthedocs.io/en/stable/user-guide/resource_tracking), you should add the following to your `spec.source.helm.values` field:
569+
570+
* When [ArgoCD tracks resources by label](https://argo-cd.readthedocs.io/en/stable/user-guide/resource_tracking/#tracking-kubernetes-resources-by-label) (default of **ArgoCD v2.X**)
571+
572+
```yaml
573+
common:
574+
globalLabels:
575+
app.kubernetes.io/instance: ARGOCD_APP_NAME
576+
globalAnnotations:
577+
argocd.argoproj.io/ignore-resource-updates: "true"
578+
argocd.argoproj.io/sync-wave: 10
579+
argocd.argoproj.io/compare-options: IgnoreExtraneous
580+
argocd.argoproj.io/sync-options: Prune=false
581+
```
582+
583+
Where **ARGOCD_APP_NAME** should be replaced with the name of your ArgoCD application.
584+
585+
* When [ArgoCD tracks resources by annotation](https://argo-cd.readthedocs.io/en/stable/user-guide/resource_tracking/#tracking-kubernetes-resources-by-annotation) (default of **ArgoCD >= v3.X**)
586+
587+
```yaml
588+
common:
589+
globalAnnotations:
590+
argocd.argoproj.io/tracking-id: ARGOCD_APP_NAME:${group}/${kind}:${namespace}/${name}
591+
argocd.argoproj.io/ignore-resource-updates: "true"
592+
argocd.argoproj.io/sync-wave: 10
593+
argocd.argoproj.io/compare-options: IgnoreExtraneous
594+
argocd.argoproj.io/sync-options: Prune=false
595+
```
596+
597+
Where **ARGOCD_APP_NAME** should be replaced with the name of your ArgoCD application.
598+
599+
The `${group}`, `${kind}`, `${namespace}` and `${name}` fields are replaced at runtime with the values of the object where annotation is applied.
600+
601+
The above instruments Liqo to add some **argocd annotations and labels** to the resources it creates.
602+
Those fields and labels have the following meaning:
561603

562-
* **argocd.argoproj.io/ignore-resource-updates: "true"**: This allows ArgoCD to ignore updates on resources created by other resources (like a *Pod* and its *Deployment*), which are managed by ArgoCD.
563-
* **argocd.argoproj.io/sync-options: Delete=true**: Allows ArgoCD to delete resources created by Liqo controllers.
564-
* **argocd.argoproj.io/sync-wave: 10**: This allows ArgoCD to wait resources deletion before deleting the Liqo controllers.
565-
* **argocd.argoproj.io/compare-options: IgnoreExtraneous**: This allows ArgoCD to ignore resources created by Liqo controllers.
604+
* labels:
605+
* **app.kubernetes.io/instance: ARGOCD_APP_NAME**: when ArgoCD tracks resource by label, it tells ArgoCD to track the resource
606+
* annotations:
607+
* **argocd.argoproj.io/tracking-id: ARGOCD_APP_NAME:${group}/${kind}:${namespace}/${name}**: when ArgoCD tracks resource by annotation, it tells ArgoCD to track the annotated resource
608+
* **argocd.argoproj.io/ignore-resource-updates: "true"**: allows ArgoCD to ignore updates on resources created by other resources (like a *Pod* and its *Deployment*), which are managed by ArgoCD.
609+
* **argocd.argoproj.io/sync-wave: 10**: allows ArgoCD to wait resources deletion before deleting the Liqo controllers.
610+
* **argocd.argoproj.io/compare-options: IgnoreExtraneous**: allows ArgoCD to ignore resources created by Liqo controllers when the sync status is determined.
611+
* **argocd.argoproj.io/sync-options: Prune=false**: prevents ArgoCD from deleting the resources managed by the Liqo controllers.
566612

567613
## CNIs
568614

pkg/utils/resource/suite_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2019-2025 The Liqo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package resource
16+
17+
import (
18+
"testing"
19+
20+
. "github.com/onsi/ginkgo/v2"
21+
. "github.com/onsi/gomega"
22+
)
23+
24+
func TestUtils(t *testing.T) {
25+
defer GinkgoRecover()
26+
RegisterFailHandler(Fail)
27+
RunSpecs(t, "Test Resource Utils")
28+
}

pkg/utils/resource/wrapper_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2019-2025 The Liqo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package resource
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
. "github.com/onsi/ginkgo/v2"
22+
. "github.com/onsi/gomega"
23+
appsv1 "k8s.io/api/apps/v1"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/client-go/kubernetes/scheme"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
29+
)
30+
31+
var _ = Describe("CreateOrUpdate Wrapper", func() {
32+
var (
33+
ctx context.Context
34+
cl client.Client
35+
obj client.Object = &appsv1.Deployment{
36+
ObjectMeta: metav1.ObjectMeta{
37+
Name: "testname",
38+
Namespace: "testns",
39+
Labels: map[string]string{"existing": "label"},
40+
Annotations: map[string]string{"existing": "annotation"},
41+
},
42+
Spec: appsv1.DeploymentSpec{
43+
Replicas: func() *int32 { i := int32(1); return &i }(),
44+
Selector: &metav1.LabelSelector{
45+
MatchLabels: map[string]string{"app": "testapp"},
46+
},
47+
Template: corev1.PodTemplateSpec{
48+
ObjectMeta: metav1.ObjectMeta{
49+
Labels: map[string]string{"app": "testapp"},
50+
},
51+
Spec: corev1.PodSpec{
52+
Containers: []corev1.Container{
53+
{
54+
Name: "testcontainer",
55+
Image: "testimage",
56+
},
57+
},
58+
},
59+
},
60+
},
61+
}
62+
)
63+
64+
BeforeEach(func() {
65+
ctx = context.TODO()
66+
cl = fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
67+
})
68+
69+
AfterEach(func() {
70+
SetGlobalLabels(nil)
71+
SetGlobalAnnotations(nil)
72+
})
73+
74+
It("should inject global labels and annotations with variable replacement", func() {
75+
SetGlobalLabels(map[string]string{
76+
"app": "liqo:${namespace}-${name}:${kind}-${group}",
77+
"env": "test",
78+
"namespace-var": "${namespace}",
79+
})
80+
SetGlobalAnnotations(map[string]string{
81+
"anno": "liqo:${namespace}-${name}:${kind}-${group}",
82+
})
83+
84+
mutateFn := func() error {
85+
// Simulate mutation logic
86+
return nil
87+
}
88+
89+
newObj := obj.DeepCopyObject().(client.Object)
90+
91+
_, err := CreateOrUpdate(ctx, cl, newObj, mutateFn)
92+
Expect(err).ToNot(HaveOccurred())
93+
94+
groupKind, _, _ := cl.Scheme().ObjectKinds(newObj)
95+
expectedReplacedString := fmt.Sprintf("liqo:%s-%s:%s-%s",
96+
newObj.GetNamespace(),
97+
newObj.GetName(),
98+
groupKind[0].Kind,
99+
groupKind[0].Group,
100+
)
101+
102+
labels := newObj.GetLabels()
103+
// Check that existing labels are preserved
104+
for k, v := range obj.GetLabels() {
105+
Expect(labels).To(HaveKeyWithValue(k, v))
106+
}
107+
Expect(labels).To(HaveKeyWithValue("app", expectedReplacedString))
108+
Expect(labels).To(HaveKeyWithValue("env", "test"))
109+
Expect(labels).To(HaveKeyWithValue("namespace-var", newObj.GetNamespace()))
110+
111+
annotations := newObj.GetAnnotations()
112+
Expect(annotations).To(HaveKeyWithValue("anno", expectedReplacedString))
113+
})
114+
115+
It("should not panic if no global labels/annotations are set", func() {
116+
mutateFn := func() error { return nil }
117+
_, err := CreateOrUpdate(ctx, cl, obj, mutateFn)
118+
Expect(err).ToNot(HaveOccurred())
119+
})
120+
})

pkg/utils/resource/wrappers.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ package resource
1616

1717
import (
1818
"context"
19+
"fmt"
20+
"strings"
1921

22+
"k8s.io/apimachinery/pkg/runtime"
23+
"k8s.io/apimachinery/pkg/runtime/schema"
2024
"sigs.k8s.io/controller-runtime/pkg/client"
2125
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2226
)
2327

28+
var knownVariables = []string{"namespace", "name", "kind", "group"}
29+
2430
// CreateOrUpdate is a wrapper around controllerutil.CreateOrUpdate that automatically adds global labels and annotations.
2531
// It takes the same parameters as controllerutil.CreateOrUpdate plus an optional list of additional labels to merge.
2632
func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object,
@@ -32,13 +38,19 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object,
3238
return err
3339
}
3440

41+
// Build the variables map from the object to replace variables in global labels and annotations.
42+
variablesMap, err := buildVariablesMap(obj, c.Scheme())
43+
if err != nil {
44+
return fmt.Errorf("unable replace variable in global labels and annotations: %w", err)
45+
}
46+
3547
// Then add global labels and any additional labels
3648
if obj.GetLabels() == nil {
3749
obj.SetLabels(make(map[string]string))
3850
}
3951
labels := obj.GetLabels()
4052
for k, v := range GetGlobalLabels() {
41-
labels[k] = v
53+
labels[k] = replaceVariables(v, variablesMap)
4254
}
4355
obj.SetLabels(labels)
4456

@@ -48,7 +60,7 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object,
4860
}
4961
annotations := obj.GetAnnotations()
5062
for k, v := range GetGlobalAnnotations() {
51-
annotations[k] = v
63+
annotations[k] = replaceVariables(v, variablesMap)
5264
}
5365
obj.SetAnnotations(annotations)
5466

@@ -57,3 +69,42 @@ func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object,
5769

5870
return controllerutil.CreateOrUpdate(ctx, c, obj, wrappedMutateFn)
5971
}
72+
73+
func getVar(variable string, obj client.Object, kind schema.GroupVersionKind) string {
74+
switch variable {
75+
case "namespace":
76+
return obj.GetNamespace()
77+
case "name":
78+
return obj.GetName()
79+
case "kind":
80+
return kind.Kind
81+
case "group":
82+
return kind.Group
83+
}
84+
85+
return ""
86+
}
87+
88+
func buildVariablesMap(obj client.Object, scheme *runtime.Scheme) (map[string]string, error) {
89+
variables := make(map[string]string)
90+
groupKind, _, err := scheme.ObjectKinds(obj)
91+
92+
if err != nil {
93+
return nil, fmt.Errorf("unable to retrieve object kind: %w", err)
94+
}
95+
96+
for _, variable := range knownVariables {
97+
variables[variable] = getVar(variable, obj, groupKind[0])
98+
}
99+
return variables, nil
100+
}
101+
102+
// replaceVariables replace the variables in the given string with the corresponding values in the object.
103+
func replaceVariables(s string, variablesMap map[string]string) string {
104+
// Replace the variables in the string with the corresponding values in the object
105+
for varName, varValue := range variablesMap {
106+
s = strings.ReplaceAll(s, "${"+varName+"}", varValue)
107+
}
108+
109+
return s
110+
}

0 commit comments

Comments
 (0)