Skip to content

Commit

Permalink
Provide a flag to set the namespace of the ArgoCD instance (#519)
Browse files Browse the repository at this point in the history
* Provide a flag to set the namespace of the ArgoCD instance

* Do not use ownerReference to app instead of the finalizer

* Avoid handling too many update events of app
  • Loading branch information
LinuxSuRen authored Mar 28, 2022
1 parent 61acdb4 commit 2ca3766
Show file tree
Hide file tree
Showing 17 changed files with 229 additions and 53 deletions.
1 change: 1 addition & 0 deletions cmd/apiserver/app/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) {
s.JenkinsOptions.AddFlags(fss.FlagSet("devops"), s.JenkinsOptions)
s.SonarQubeOptions.AddFlags(fss.FlagSet("sonarqube"), s.SonarQubeOptions)
s.S3Options.AddFlags(fss.FlagSet("s3"), s.S3Options)
s.ArgoCDOption.AddFlags(fss.FlagSet("argocd"))

fs = fss.FlagSet("klog")
local := flag.NewFlagSet("klog", flag.ExitOnError)
Expand Down
3 changes: 3 additions & 0 deletions cmd/apiserver/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func NewAPIServerCommand() (cmd *cobra.Command) {
// Load configuration from file
conf, err := config.TryLoadFromDisk()
if err == nil {
if conf.ArgoCDOption == nil {
conf.ArgoCDOption = &config.ArgoCDOption{}
}
s = &options.ServerRunOptions{
GenericServerRunOptions: s.GenericServerRunOptions,
Config: conf,
Expand Down
1 change: 1 addition & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ rules:
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
Expand Down
114 changes: 101 additions & 13 deletions controllers/argocd/application_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -28,10 +29,12 @@ import (
"kubesphere.io/devops/pkg/utils/k8sutil"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

//+kubebuilder:rbac:groups=gitops.kubesphere.io,resources=applications,verbs=get;list;update
//+kubebuilder:rbac:groups=argoproj.io,resources=applications,verbs=get;list;create;update
//+kubebuilder:rbac:groups=argoproj.io,resources=applications,verbs=get;list;create;update;delete
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch

// ApplicationReconciler is the reconciler of the Application
type ApplicationReconciler struct {
Expand All @@ -41,6 +44,10 @@ type ApplicationReconciler struct {
}

// Reconcile makes sure the Application and ArgoCD application works well
// Consider all the ArgoCD application need to be in one particular namespace. But the Application from this project can be in different namespaces.
// In order to avoid the naming conflict, we will check if the name existing the target namespace. Take the original name
// as the generatedName of the ArgoCD application if there is a potential conflict. In the most cases, we can keep the original name
// same to the ArgoCD Application name.
func (r *ApplicationReconciler) Reconcile(req ctrl.Request) (result ctrl.Result, err error) {
ctx := context.Background()
r.log.Info(fmt.Sprintf("start to reconcile application: %s", req.String()))
Expand All @@ -61,10 +68,46 @@ func (r *ApplicationReconciler) reconcileArgoApplication(app *v1alpha1.Applicati
ctx := context.Background()

argoApp := createBareArgoCDApplicationObject()
argoCDNamespace, ok := app.Labels[v1alpha1.ArgoCDLocationLabelKey]
if !ok || "" == argoCDNamespace {
r.recorder.Eventf(app, corev1.EventTypeWarning, "Invalid",
"Cannot find the namespace of the Argo CD instance from key: %s", v1alpha1.ArgoCDLocationLabelKey)
return
}
argoCDAppName, hasArgoName := app.Labels[v1alpha1.ArgoCDAppNameLabelKey]
if argoCDAppName == "" {
argoCDAppName = app.GetName()
}

// the application was deleted
if !app.ObjectMeta.DeletionTimestamp.IsZero() {
if err = r.Get(ctx, types.NamespacedName{
Namespace: argoCDNamespace,
Name: argoCDAppName,
}, argoApp); err != nil {
if !apierrors.IsNotFound(err) {
return
}
err = nil
} else {
err = r.Delete(ctx, argoApp)
}

if err == nil {
k8sutil.RemoveFinalizer(&app.ObjectMeta, v1alpha1.ApplicationFinalizerName)
err = r.Update(context.TODO(), app)
}
return
}
if k8sutil.AddFinalizer(&app.ObjectMeta, v1alpha1.ApplicationFinalizerName) {
if err = r.Update(context.TODO(), app); err != nil {
return
}
}

if err = r.Get(ctx, types.NamespacedName{
Namespace: app.GetNamespace(),
Name: app.GetName(),
Namespace: argoCDNamespace,
Name: argoCDAppName,
}, argoApp); err != nil {
if !apierrors.IsNotFound(err) {
return
Expand All @@ -74,13 +117,40 @@ func (r *ApplicationReconciler) reconcileArgoApplication(app *v1alpha1.Applicati
return
}

err = r.Create(ctx, argoApp)
argoApp.SetName(app.Name)
argoApp.SetNamespace(argoCDNamespace)
if err = r.Create(ctx, argoApp); err == nil {
err = r.addArgoAppNameIntoLabels(app.GetNamespace(), app.GetName(), argoApp.GetName())
} else {
data, _ := argoApp.MarshalJSON()
r.log.Error(err, "failed to create application to ArgoCD", "data", string(data))
r.recorder.Eventf(app, corev1.EventTypeWarning, "FailedWithArgoCD",
"failed to create application to ArgoCD, error is: %v", err)
}
} else {
var newArgoApp *unstructured.Unstructured
if newArgoApp, err = createUnstructuredApplication(app); err == nil {
argoApp.Object["spec"] = newArgoApp.Object["spec"]
k8sutil.AddOwnerReference(argoApp, app.TypeMeta, app.ObjectMeta)
err = r.Update(ctx, argoApp)
if !hasArgoName {
// naming conflict happened
if argoApp, err = createUnstructuredApplication(app); err != nil {
return
}
argoApp.SetGenerateName(app.GetName())
argoApp.SetName("")
argoApp.SetNamespace(argoCDNamespace)

if err = r.Create(ctx, argoApp); err == nil {
err = r.addArgoAppNameIntoLabels(app.GetNamespace(), app.GetName(), argoApp.GetName())
} else {
data, _ := argoApp.MarshalJSON()
r.log.Error(err, "failed to create application to ArgoCD", "data", string(data))
r.recorder.Eventf(app, corev1.EventTypeWarning, "FailedWithArgoCD",
"failed to create application to ArgoCD, error is: %v", err)
}
} else {
var newArgoApp *unstructured.Unstructured
if newArgoApp, err = createUnstructuredApplication(app); err == nil {
argoApp.Object["spec"] = newArgoApp.Object["spec"]
err = r.Update(ctx, argoApp)
}
}
}
return
Expand All @@ -103,6 +173,11 @@ func createUnstructuredApplication(app *v1alpha1.Application) (result *unstructu
}

newArgoApp := createBareArgoCDApplicationObject()
newArgoApp.SetLabels(map[string]string{
v1alpha1.ArgoCDAppControlByLabelKey: "ks-devops",
v1alpha1.AppNamespaceLabelKey: app.Namespace,
v1alpha1.AppNameLabelKey: app.Name,
})
newArgoApp.SetName(app.GetName())
newArgoApp.SetNamespace(app.GetNamespace())

Expand All @@ -115,13 +190,25 @@ func createUnstructuredApplication(app *v1alpha1.Application) (result *unstructu
return nil, err
}
}

// set owner reference
k8sutil.AddOwnerReference(newArgoApp, app.TypeMeta, app.ObjectMeta)

return newArgoApp, nil
}

func (r *ApplicationReconciler) addArgoAppNameIntoLabels(namespace, name, argoAppName string) (err error) {
app := &v1alpha1.Application{}
ctx := context.Background()
if err = r.Client.Get(ctx, types.NamespacedName{
Namespace: namespace,
Name: name,
}, app); err == nil {
if app.Labels == nil {
app.Labels = make(map[string]string)
}
app.Labels[v1alpha1.ArgoCDAppNameLabelKey] = argoAppName
err = r.Update(ctx, app)
}
return
}

// GetName returns the name of this reconciler
func (r *ApplicationReconciler) GetName() string {
return "ApplicationController"
Expand All @@ -138,6 +225,7 @@ func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.log = ctrl.Log.WithName(r.GetName())
r.recorder = mgr.GetEventRecorderFor(r.GetName())
return ctrl.NewControllerManagedBy(mgr).
WithEventFilter(predicate.GenerationChangedPredicate{}).
For(&v1alpha1.Application{}).
Complete(r)
}
22 changes: 9 additions & 13 deletions controllers/argocd/application_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"kubesphere.io/devops/pkg/api/devops/v1alpha3"
"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
Expand Down Expand Up @@ -58,11 +57,6 @@ func Test_createUnstructuredApplication(t *testing.T) {
},
verify: func(t *testing.T, gotResult *unstructured.Unstructured, gotErr error) {
assert.Nil(t, gotErr)

// make sure it has the ownerReference
refs, _, _ := unstructured.NestedSlice(gotResult.Object, "metadata", "ownerReferences")
assert.NotNil(t, refs)
assert.Equal(t, 1, len(refs))
},
}, {
name: "with some specific fields, with default values",
Expand Down Expand Up @@ -100,13 +94,16 @@ func Test_createUnstructuredApplication(t *testing.T) {
}

func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
schema, err := v1alpha3.SchemeBuilder.Register().Build()
schema, err := v1alpha1.SchemeBuilder.Register().Build()
assert.Nil(t, err)

app := &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "fake",
Namespace: "fake",
Labels: map[string]string{
v1alpha1.ArgoCDLocationLabelKey: "fake",
},
},
Spec: v1alpha1.ApplicationSpec{
ArgoApp: &v1alpha1.ArgoApplication{
Expand All @@ -129,9 +126,8 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
argoApp.SetNamespace("fake")

type fields struct {
Client client.Client
log logr.Logger
recorder record.EventRecorder
Client client.Client
log logr.Logger
}
type args struct {
app *v1alpha1.Application
Expand All @@ -144,7 +140,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
}{{
name: "without Argo Application",
fields: fields{
Client: fake.NewFakeClientWithScheme(schema),
Client: fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
},
args: args{
app: app.DeepCopy(),
Expand All @@ -167,7 +163,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
}, {
name: "with Argo Application",
fields: fields{
Client: fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
Client: fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
},
args: args{
app: app.DeepCopy(),
Expand All @@ -191,7 +187,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
r := &ApplicationReconciler{
Client: tt.fields.Client,
log: tt.fields.log,
recorder: tt.fields.recorder,
recorder: &record.FakeRecorder{},
}
err := r.reconcileArgoApplication(tt.args.app)
tt.verify(t, tt.fields.Client, err)
Expand Down
24 changes: 22 additions & 2 deletions controllers/argocd/argocd-application-status-controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ package argocd
import (
"context"
"encoding/json"
"fmt"
"github.com/go-logr/logr"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
"reflect"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

//+kubebuilder:rbac:groups=gitops.kubesphere.io,resources=applications,verbs=get;update
Expand All @@ -44,14 +48,26 @@ type ApplicationStatusReconciler struct {
// Reconcile is the entrypoint of the controller
func (r *ApplicationStatusReconciler) Reconcile(req ctrl.Request) (result ctrl.Result, err error) {
var argoCDApp *unstructured.Unstructured
r.log.Info(fmt.Sprintf("start to reconcile ArgoCD application: %s", req.String()))

if argoCDApp, err = getArgoCDApplication(r.Client, req.NamespacedName); err != nil {
err = client.IgnoreNotFound(err)
return
}

appNs := argoCDApp.GetLabels()[v1alpha1.AppNamespaceLabelKey]
appName := argoCDApp.GetLabels()[v1alpha1.AppNameLabelKey]
if appName == "" || appNs == "" {
return
}

ctx := context.Background()
app := &v1alpha1.Application{}
if err = r.Get(ctx, req.NamespacedName, app); err != nil {
if err = r.Get(ctx, types.NamespacedName{
Namespace: appNs,
Name: appName,
}, app); err != nil {
r.log.Error(err, "cannot find application with namespace: %s, name: %s", appNs, appName)
err = nil
return
}
Expand Down Expand Up @@ -86,7 +102,6 @@ func (r *ApplicationStatusReconciler) Reconcile(req ctrl.Request) (result ctrl.R
return
}

// update status subresource
app.Status.ArgoApp = string(statusData)
err = r.Status().Update(ctx, app)
}
Expand Down Expand Up @@ -128,7 +143,12 @@ func (r *ApplicationStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
argoApp := createBareArgoCDApplicationObject()
r.log = ctrl.Log.WithName(r.GetName())
r.recorder = mgr.GetEventRecorderFor(r.GetName())
var withLabelPredicate = predicate.NewPredicateFuncs(func(meta metav1.Object, object runtime.Object) (ok bool) {
_, ok = meta.GetLabels()[v1alpha1.ArgoCDAppControlByLabelKey]
return
})
return ctrl.NewControllerManagedBy(mgr).
For(argoApp).
WithEventFilter(withLabelPredicate).
Complete(r)
}
10 changes: 4 additions & 6 deletions controllers/argocd/argocd-application-status-controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package argocd

import (
"fmt"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand All @@ -29,6 +28,7 @@ import (
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/log"
"testing"
)

Expand Down Expand Up @@ -123,9 +123,7 @@ func TestArgoCDApplicationStatusReconciler_Reconcile(t *testing.T) {
defaultArgoCDApp.SetNamespace("ns")

type fields struct {
Client client.Client
log logr.Logger
recorder record.EventRecorder
Client client.Client
}
type args struct {
req controllerruntime.Request
Expand Down Expand Up @@ -192,8 +190,8 @@ func TestArgoCDApplicationStatusReconciler_Reconcile(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
r := &ApplicationStatusReconciler{
Client: tt.fields.Client,
log: tt.fields.log,
recorder: tt.fields.recorder,
log: log.NullLogger{},
recorder: &record.FakeRecorder{},
}
gotResult, err := r.Reconcile(tt.args.req)
if !tt.wantErr(t, err, fmt.Sprintf("Reconcile(%v)", tt.args.req)) {
Expand Down
Loading

0 comments on commit 2ca3766

Please sign in to comment.