Skip to content

Commit

Permalink
Feature custom healthcheck (#3)
Browse files Browse the repository at this point in the history
* Initial changes for customhealth check

Signed-off-by: rahul-mourya <[email protected]>

* added integration tests

Signed-off-by: rahul-mourya <[email protected]>

* updated kuttl tests config

Signed-off-by: rahul-mourya <[email protected]>

* Added Unit Test cases and some cleanup

Signed-off-by: rahul-mourya <[email protected]>

* remove duplicate file

Signed-off-by: rahul-mourya <[email protected]>

* updated comment

Signed-off-by: rahul-mourya <[email protected]>

* added ssh startup command

Signed-off-by: rahul-mourya <[email protected]>

* Added impl for accessing GIT over HTTPS

Signed-off-by: rahul-mourya <[email protected]>

* updated ssh_known_host changes

Signed-off-by: rahul-mourya <[email protected]>

* Updated readme for e2e testcases

Signed-off-by: rahul-mourya <[email protected]>

* removed commented code

Signed-off-by: rahul-mourya <[email protected]>

* Updated ex secret for accessing local git repo

Signed-off-by: rahul-mourya <[email protected]>

* Update README.md

Signed-off-by: rahul-mourya <[email protected]>

---------

Signed-off-by: rahul-mourya <[email protected]>
  • Loading branch information
rahul-mourya committed Jun 6, 2023
1 parent 40fcc74 commit ec7fc07
Show file tree
Hide file tree
Showing 93 changed files with 3,937 additions and 203 deletions.
64 changes: 64 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ArgoCD Extensions

Forked from [argoproj-labs/argocd-extensions](https://github.com/argoproj-labs/argocd-extensions), commit [94e4126](https://github.com/argoproj-labs/argocd-extensions/tree/94e41261793f27c51de38f5c5544b1bb05a9e2e5).

## v0.1.0 (Base)

The ArgoCD Extensions operator was that it would watch for
`ArgoCDExtensions` CRs to be created, and it would then download all files within the `resources/`
directory. After an extension's resources files are downloaded, those files would be then moved into
a shared extensions directory.

The following sections detail the enhancements made to ArgoCD Extensions.

## v1.0.0 (Fork)

### Private GitHub Authentication

Adds support for authenticating with GitHub to access private repositories.

The `ArgoCDExtension` CRD has been modified to allow for a Kubernetes secret to be provided by its name and namespace.
A matching Kubernetes secret must be created with a valid SSH key as `data.sshkey`.

### Resource Customization ConfigMap

Adds resource customization ConfigMap in order to integrate with the ArgoCD server.

There was no existing functionality to integrate health checks, so a new ConfigMap has been introduced. This ConfigMap
is entirely owned by the ArgoCD Extensions operator and will be regenerated whenever the operator reconciles. After each
download of an ArgoCD Extension, the resource customizations will be read in from the file system and then placed into
this ConfigMap.

### Configurable Base Directory

The default base directory for a GitHub repo is `resources`. In order to be more flexible and allow for different
repository structures, this can now be set in the `ArgoCDExtension` CR as `spec.baseDirectory`.

### Finalizer

Adds handling for the deletion of an `ArgoCDExtension` CR.

On deleting an `ArgoCDExtension` CR, the operator will delete all files that are part of the snapshot stored for the
extension. Deletion will fail if the operator attempts to delete a file that isn't own by this extension. Once all files
are deleted, the ConfigMap is rebuilt and the finalizer is removed.

### Resource Conflicts

Adds handling for resource conflicts.

It is possible for two extensions to have the same resource file. Since all extensions are
eventually moved into a shared extensions folder, the order in which extensions are downloaded could
change the resulting files in the shared resources directory. If two extensions contain the same file,
then the resulting file would be from the most recently downloaded extension.

The Automation Platform fork of ArgoCD _prevents extensions from overwritting files owned by other extensions_.
This means that if two extensions contain the same file, then the first extension to be loaded will own the file.
The second extension will receive an error as it is not possible to load it without potentially breaking the existing
extension.

Ownership is tracked using a file tracker. Whenever a file is downloaded, it is added to the file tracker. If the file
is already being tracked, then an error is thrown if the file isn't owned by current extension. If the file is owned by
current extension, then the file is overwritten and the file tracker is unchanged.

Preventing resource conflicts is critical to ensure that extensions owned by one team are not capable of
breaking extensions owned by another team.
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ FROM alpine:latest

RUN apk update && apk upgrade && apk add git openssh-client

# support for mounting configuration from a configmap
WORKDIR /app/config/ssh
RUN touch ssh_known_hosts && \
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts

WORKDIR /
COPY --from=builder /workspace/manager .
RUN adduser --disabled-password -u 65532 main 65532
USER 65532:65532

ENTRYPOINT ["/manager"]
12 changes: 12 additions & 0 deletions api/v1alpha1/argocdextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
type ArgoCDExtensionSpec struct {
// Sources specifies where the extension should come from
Sources []ExtensionSource `json:"sources"`
// BaseDirectory specifies the directory that all resource customizations are stored under
BaseDirectory string `json:"baseDirectory"`
}

type ArgoCDExtensionConditionType string
Expand Down Expand Up @@ -68,10 +70,20 @@ type GitSource struct {
Url string `json:"url,omitempty"`
// Revision specifies the revision of the Repository to fetch
Revision string `json:"revision,omitempty"`
// Secret specifies the Kubernetes secret to use when authenticating with GitHub
Secret *NamespacedName `json:"secret,omitempty"`
}

// WebSource specifies a repo that holds an extension
type WebSource struct {
// URK specifies the remote file URL
Url string `json:"url,omitempty"`
}

// NamespacedName specifies a Kubernetes resource by its namespace and name
type NamespacedName struct {
// Namespace specifies the namespace of the Kubernetes resource
Namespace string `json:"namespace"`
// Name specifies the name of the Kubernetes resource
Name string `json:"name"`
}
52 changes: 37 additions & 15 deletions controllers/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

extensionv1 "github.com/argoproj/argocd-extensions/api/v1alpha1"
"github.com/argoproj/argocd-extensions/pkg/extension"
Expand All @@ -18,47 +19,68 @@ const (
finalizerName = "extensions-finalizer.argocd.argoproj.io"
)

var (
controllerLog = ctrl.Log.WithName("controller")
)

// ArgoCDExtensionReconciler reconciles a ArgoCDExtension object
type ArgoCDExtensionReconciler struct {
client.Client
Scheme *runtime.Scheme
ExtensionsPath string
}

func findIndex(in []string, item string) int {
for i := range in {
if in[i] == item {
return i
}
}
return -1
}

func (r *ArgoCDExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var original extensionv1.ArgoCDExtension
if err := r.Get(ctx, req.NamespacedName, &original); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
ext := original.DeepCopy()

extensionCtx := extension.NewExtensionContext(ext, r.ExtensionsPath)
extensionLog := controllerLog.WithValues("extensionName", ext.Name)
extensionCtx := extension.NewExtensionContext(ext, r.Client, r.ExtensionsPath)

if index := findIndex(ext.Finalizers, finalizerName); index > -1 && ext.DeletionTimestamp != nil {
if err := extensionCtx.ProcessDeletion(); err != nil {
isMarkedForDeletion := ext.GetDeletionTimestamp() != nil
if isMarkedForDeletion {
if controllerutil.ContainsFinalizer(ext, finalizerName) {
extensionLog.Info("processing deletion...")
if err := extensionCtx.ProcessDeletion(ctx); err != nil {
extensionLog.Error(err, "failed to process deletion")
return ctrl.Result{}, err
}
extensionLog.Info("removing finalizer...")
controllerutil.RemoveFinalizer(ext, finalizerName)
err := r.Client.Update(ctx, ext)
if err != nil {
extensionLog.Error(err, "failed to remove finalizer")
return ctrl.Result{}, err
}
extensionLog.Info("removed finalizer")
}
return ctrl.Result{}, nil
}

// Add finalizer for this CR
if !controllerutil.ContainsFinalizer(ext, finalizerName) {
controllerutil.AddFinalizer(ext, finalizerName)
err := r.Update(ctx, ext)
if err != nil {
extensionLog.Error(err, "failed to add finalizer")
return ctrl.Result{}, err
}
ext.Finalizers = append(ext.Finalizers[:index], ext.Finalizers[index+1:]...)
err := r.Client.Update(ctx, ext)
return ctrl.Result{}, err
extensionLog.Info("added finalizer")
}

readyCondition := extensionv1.ArgoCDExtensionCondition{Type: extensionv1.ConditionReady}
extensionLog.Info("processing...")
if err := extensionCtx.Process(ctx); err != nil {
readyCondition.Status = metav1.ConditionFalse
readyCondition.Message = err.Error()
extensionLog.Error(err, "failed to process")
} else {
readyCondition.Status = metav1.ConditionTrue
readyCondition.Message = fmt.Sprintf("Successfully processed %d extension sources", len(original.Spec.Sources))
extensionLog.Info("successfully processed", "sourceCount", len(original.Spec.Sources))
}
ext.Status.Conditions = []extensionv1.ArgoCDExtensionCondition{readyCondition}
if !reflect.DeepEqual(ext.Status, original.Status) {
Expand Down
86 changes: 86 additions & 0 deletions controllers/controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package controllers

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
crtclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
extensionv1 "github.com/argoproj/argocd-extensions/api/v1alpha1"
)

func TestReconcilerValidationErrorBehaviour(t *testing.T) {

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret",
Namespace: "argocd",
},
Data: map[string][]byte{
"sshkey": []byte("LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNCeFBxMHE0U1c5RGJmY0oxL09jdE9RajZ2NDNaTUpVckZqc1JXYTJMaVFsQUFBQUtCbUJERlZaZ1F4ClZRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQnhQcTBxNFNXOURiZmNKMS9PY3RPUWo2djQzWk1KVXJGanNSV2EyTGlRbEEKQUFBRURCT3g5RmVIc3ZTcjBSdzhVcEIwM2VPOU8wRlN0bHNPOWRGSzZ4cGJKREUzRStyU3JoSmIwTnQ5d25YODV5MDVDUApxL2pka3dsU3NXT3hGWnJZdUpDVUFBQUFIWEp0YjNWQVVtRm9kV3h6TFUxaFkwSnZiMnN0VUhKdkxteHZZMkZzCi0tLS0tRU5EIE9QRU5TU0ggUFJJVkFURSBLRVktLS0tLQo="),
},
}

scheme := runtime.NewScheme()
scheme.AddKnownTypes(corev1.SchemeGroupVersion, secret)
err := extensionv1.AddToScheme(scheme)
assert.Nil(t, err)
err = argov1alpha1.AddToScheme(scheme)
assert.Nil(t, err)

validargocdExtension := extensionv1.ArgoCDExtension{
ObjectMeta: metav1.ObjectMeta{
Name: "test-valid-argocdextension",
Namespace: "argocd",
},
Spec: extensionv1.ArgoCDExtensionSpec{
Sources: []extensionv1.ExtensionSource{
{
Git: &extensionv1.GitSource{
Url: "https://github.com/argoproj/argo-cd.git",
Revision: "HEAD",
Secret: &extensionv1.NamespacedName{
Name: "my-secret",
Namespace: "argocd",
},
},
},
},
BaseDirectory: "resource_customizations/argoproj.io/ApplicationSet",
},
}

client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&validargocdExtension, secret).Build()

r := ArgoCDExtensionReconciler{
Client: client,
Scheme: scheme,
ExtensionsPath: "/tmp/resource",
}

req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "argocd",
Name: "test-valid-argocdextension",
},
}

res, err := r.Reconcile(context.Background(), req)
assert.Nil(t, err)
assert.True(t, res.RequeueAfter == 0)

var argocdExtension extensionv1.ArgoCDExtension

// make sure argocd extension got created
err = r.Client.Get(context.TODO(), crtclient.ObjectKey{Namespace: "argocd", Name: "test-valid-argocdextension"}, &argocdExtension)
assert.NoError(t, err)
assert.Equal(t, argocdExtension, "test-valid-argocdextension")
}
Loading

0 comments on commit ec7fc07

Please sign in to comment.