feat: Merge applicationset into argocd (#8864)

feat: Merge applicationset into argocd (#8864)

Signed-off-by: rishabh625 <rishabhmishra625@gmail.com>
Co-authored-by: jannfis <jann@mistrust.net>
This commit is contained in:
rishabh625 2022-03-30 03:41:02 +05:30 committed by GitHub
parent 8847a310ad
commit c77cf66aa1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 23270 additions and 14429 deletions

View file

@ -407,11 +407,12 @@ jobs:
# port 8080 which is not visible in netstat -tulpen, but still there
# with a HTTP listener. We have API server listening on port 8088
# instead.
make controller repo-server server applicationset-controller
make start-e2e-local 2>&1 | sed -r "s/[[:cntrl:]]\[[0-9]{1,3}m//g" > /tmp/e2e-server.log &
count=1
until curl -f http://127.0.0.1:8088/healthz; do
sleep 10;
if test $count -ge 120; then
if test $count -ge 180; then
echo "Timeout"
exit 1
fi
@ -426,4 +427,4 @@ jobs:
with:
name: e2e-server-k8s${{ matrix.k3s-version }}.log
path: /tmp/e2e-server.log
if: ${{ failure() }}
if: ${{ failure() }}

View file

@ -128,5 +128,6 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-cmp-server
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-application-controller
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-dex
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-notifications
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-applicationset-controller
USER 999

View file

@ -10,4 +10,6 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-repo-server
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-application-controller
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-dex
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-notifications
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-applicationset-controller
USER 999

View file

@ -421,6 +421,7 @@ start-e2e-local: mod-vendor-local dep-ui-local
kubectl create ns argocd-e2e || true
kubectl config set-context --current --namespace=argocd-e2e
kustomize build test/manifests/base | kubectl apply -f -
kubectl apply -f https://raw.githubusercontent.com/open-cluster-management/api/a6845f2ebcb186ec26b832f60c988537a58f3859/cluster/v1alpha1/0000_04_clusters.open-cluster-management.io_placementdecisions.crd.yaml
# Create GPG keys and source directories
if test -d /tmp/argo-e2e/app/config/gpg; then rm -rf /tmp/argo-e2e/app/config/gpg/*; fi
mkdir -p /tmp/argo-e2e/app/config/gpg/keys && chmod 0700 /tmp/argo-e2e/app/config/gpg/keys
@ -557,3 +558,7 @@ start-test-k8s:
.PHONY: list
list:
@LC_ALL=C $(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$'
.PHONY: applicationset-controller
applicationset-controller:
CGO_ENABLED=0 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-applicationset-controller ./cmd

View file

@ -8,3 +8,4 @@ ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
git-server: test/fixture/testrepos/start-git.sh
helm-registry: test/fixture/testrepos/start-helm-registry.sh
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
applicationset-controller: [ "$BIN_MODE" == 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-applicationset-controller $COMMAND --loglevel debug --metrics-addr localhost:12345 --probe-addr localhost:12346 --argocd-repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"

View file

@ -0,0 +1,724 @@
/*
Licensed 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 controllers
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/argoproj/argo-cd/v2/applicationset/generators"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
"github.com/argoproj/argo-cd/v2/common"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/db"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
argoutil "github.com/argoproj/argo-cd/v2/util/argo"
apierr "k8s.io/apimachinery/pkg/api/errors"
)
const (
// Rather than importing the whole argocd-notifications controller, just copying the const here
// https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/subscriptions.go#L12
// https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/state.go#L17
NotifiedAnnotationKey = "notified.notifications.argoproj.io"
ReconcileRequeueOnValidationError = time.Minute * 3
)
// ApplicationSetReconciler reconciles a ApplicationSet object
type ApplicationSetReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Recorder record.EventRecorder
Generators map[string]generators.Generator
ArgoDB db.ArgoDB
ArgoAppClientset appclientset.Interface
KubeClientset kubernetes.Interface
utils.Policy
utils.Renderer
}
// +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets/status,verbs=get;update;patch
func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = r.Log.WithValues("applicationset", req.NamespacedName)
_ = log.WithField("applicationset", req.NamespacedName)
var applicationSetInfo argoprojiov1alpha1.ApplicationSet
parametersGenerated := false
if err := r.Get(ctx, req.NamespacedName, &applicationSetInfo); err != nil {
if client.IgnoreNotFound(err) != nil {
log.WithError(err).Infof("unable to get ApplicationSet: '%v' ", err)
}
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Do not attempt to further reconcile the ApplicationSet if it is being deleted.
if applicationSetInfo.ObjectMeta.DeletionTimestamp != nil {
return ctrl.Result{}, nil
}
// Log a warning if there are unrecognized generators
utils.CheckInvalidGenerators(&applicationSetInfo)
// desiredApplications is the main list of all expected Applications from all generators in this appset.
desiredApplications, applicationSetReason, err := r.generateApplications(applicationSetInfo)
if err != nil {
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
Message: err.Error(),
Reason: string(applicationSetReason),
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
return ctrl.Result{}, err
}
parametersGenerated = true
validateErrors, err := r.validateGeneratedApplications(ctx, desiredApplications, applicationSetInfo, req.Namespace)
if err != nil {
// While some generators may return an error that requires user intervention,
// other generators reference external resources that may change to cause
// the error to no longer occur. We thus log the error and requeue
// with a timeout to give this another shot at a later time.
//
// Changes to watched resources will cause this to be reconciled sooner than
// the RequeueAfter time.
log.Errorf("error occurred during application validation: %s", err.Error())
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
Message: err.Error(),
Reason: argoprojiov1alpha1.ApplicationSetReasonApplicationValidationError,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
return ctrl.Result{RequeueAfter: ReconcileRequeueOnValidationError}, nil
}
var validApps []argov1alpha1.Application
for i := range desiredApplications {
if validateErrors[i] == nil {
validApps = append(validApps, desiredApplications[i])
}
}
if len(validateErrors) > 0 {
var message string
for _, v := range validateErrors {
message = v.Error()
log.Errorf("validation error found during application validation: %s", message)
}
if len(validateErrors) > 1 {
// Only the last message gets added to the appset status, to keep the size reasonable.
message = fmt.Sprintf("%s (and %d more)", message, len(validateErrors)-1)
}
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
Message: message,
Reason: argoprojiov1alpha1.ApplicationSetReasonApplicationValidationError,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
}
if r.Policy.Update() {
err = r.createOrUpdateInCluster(ctx, applicationSetInfo, validApps)
if err != nil {
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
Message: err.Error(),
Reason: argoprojiov1alpha1.ApplicationSetReasonUpdateApplicationError,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
return ctrl.Result{}, err
}
} else {
err = r.createInCluster(ctx, applicationSetInfo, validApps)
if err != nil {
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
Message: err.Error(),
Reason: argoprojiov1alpha1.ApplicationSetReasonCreateApplicationError,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
return ctrl.Result{}, err
}
}
if r.Policy.Delete() {
err = r.deleteInCluster(ctx, applicationSetInfo, desiredApplications)
if err != nil {
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
Message: err.Error(),
Reason: argoprojiov1alpha1.ApplicationSetReasonDeleteApplicationError,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
return ctrl.Result{}, err
}
}
if applicationSetInfo.RefreshRequired() {
delete(applicationSetInfo.Annotations, common.AnnotationApplicationSetRefresh)
err := r.Client.Update(ctx, &applicationSetInfo)
if err != nil {
log.Warnf("error occurred while updating ApplicationSet: %v", err)
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
Message: err.Error(),
Reason: argoprojiov1alpha1.ApplicationSetReasonRefreshApplicationError,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
return ctrl.Result{}, err
}
}
requeueAfter := r.getMinRequeueAfter(&applicationSetInfo)
log.WithField("requeueAfter", requeueAfter).Info("end reconcile")
if len(validateErrors) == 0 {
if err := r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
Message: "All applications have been generated successfully",
Reason: argoprojiov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{
RequeueAfter: requeueAfter,
}, nil
}
func getParametersGeneratedCondition(parametersGenerated bool, message string) argoprojiov1alpha1.ApplicationSetCondition {
var paramtersGeneratedCondition argoprojiov1alpha1.ApplicationSetCondition
if parametersGenerated {
paramtersGeneratedCondition = argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionParametersGenerated,
Message: "Successfully generated parameters for all Applications",
Reason: argoprojiov1alpha1.ApplicationSetReasonParametersGenerated,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}
} else {
paramtersGeneratedCondition = argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionParametersGenerated,
Message: message,
Reason: argoprojiov1alpha1.ApplicationSetReasonErrorOccurred,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusFalse,
}
}
return paramtersGeneratedCondition
}
func getResourceUpToDateCondition(errorOccurred bool, message string, reason string) argoprojiov1alpha1.ApplicationSetCondition {
var resourceUpToDateCondition argoprojiov1alpha1.ApplicationSetCondition
if errorOccurred {
resourceUpToDateCondition = argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
Message: message,
Reason: reason,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusFalse,
}
} else {
resourceUpToDateCondition = argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
Message: "ApplicationSet up to date",
Reason: argoprojiov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
}
}
return resourceUpToDateCondition
}
func (r *ApplicationSetReconciler) setApplicationSetStatusCondition(ctx context.Context, applicationSet *argoprojiov1alpha1.ApplicationSet, condition argoprojiov1alpha1.ApplicationSetCondition, paramtersGenerated bool) error {
// check if error occurred during reconcile process
errOccurred := condition.Type == argoprojiov1alpha1.ApplicationSetConditionErrorOccurred
var errOccurredCondition argoprojiov1alpha1.ApplicationSetCondition
if errOccurred {
errOccurredCondition = condition
} else {
errOccurredCondition = argoprojiov1alpha1.ApplicationSetCondition{
Type: argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
Message: "Successfully generated parameters for all Applications",
Reason: argoprojiov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
Status: argoprojiov1alpha1.ApplicationSetConditionStatusFalse,
}
}
paramtersGeneratedCondition := getParametersGeneratedCondition(paramtersGenerated, condition.Message)
resourceUpToDateCondition := getResourceUpToDateCondition(errOccurred, condition.Message, condition.Reason)
newConditions := []argoprojiov1alpha1.ApplicationSetCondition{errOccurredCondition, paramtersGeneratedCondition, resourceUpToDateCondition}
needToUpdateConditions := false
for _, condition := range newConditions {
// do nothing if appset already has same condition
for _, c := range applicationSet.Status.Conditions {
if c.Type == condition.Type && (c.Reason != condition.Reason || c.Status != condition.Status || c.Message != condition.Message) {
needToUpdateConditions = true
break
}
}
}
evaluatedTypes := map[argoprojiov1alpha1.ApplicationSetConditionType]bool{
argoprojiov1alpha1.ApplicationSetConditionErrorOccurred: true,
argoprojiov1alpha1.ApplicationSetConditionParametersGenerated: true,
argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate: true,
}
if needToUpdateConditions || len(applicationSet.Status.Conditions) < 3 {
// fetch updated Application Set object before updating it
namespacedName := types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}
if err := r.Get(ctx, namespacedName, applicationSet); err != nil {
if client.IgnoreNotFound(err) != nil {
return nil
}
return fmt.Errorf("error fetching updated application set: %v", err)
}
applicationSet.Status.SetConditions(
newConditions, evaluatedTypes,
)
// Update the newly fetched object with new set of conditions
err := r.Client.Status().Update(ctx, applicationSet)
if err != nil && !apierr.IsNotFound(err) {
return fmt.Errorf("unable to set application set condition: %v", err)
}
}
return nil
}
// validateGeneratedApplications uses the Argo CD validation functions to verify the correctness of the
// generated applications.
func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Context, desiredApplications []argov1alpha1.Application, applicationSetInfo argoprojiov1alpha1.ApplicationSet, namespace string) (map[int]error, error) {
errorsByIndex := map[int]error{}
namesSet := map[string]bool{}
for i, app := range desiredApplications {
if !namesSet[app.Name] {
namesSet[app.Name] = true
} else {
errorsByIndex[i] = fmt.Errorf("ApplicationSet %s contains applications with duplicate name: %s", applicationSetInfo.Name, app.Name)
continue
}
proj, err := r.ArgoAppClientset.ArgoprojV1alpha1().AppProjects(namespace).Get(ctx, app.Spec.GetProject(), metav1.GetOptions{})
if err != nil {
if apierr.IsNotFound(err) {
errorsByIndex[i] = fmt.Errorf("application references project %s which does not exist", app.Spec.Project)
continue
}
return nil, err
}
if err := utils.ValidateDestination(ctx, &app.Spec.Destination, r.KubeClientset, namespace); err != nil {
errorsByIndex[i] = fmt.Errorf("application destination spec is invalid: %s", err.Error())
continue
}
conditions, err := argoutil.ValidatePermissions(ctx, &app.Spec, proj, r.ArgoDB)
if err != nil {
return nil, err
}
if len(conditions) > 0 {
errorsByIndex[i] = fmt.Errorf("application spec is invalid: %s", argoutil.FormatAppConditions(conditions))
continue
}
}
return errorsByIndex, nil
}
func (r *ApplicationSetReconciler) getMinRequeueAfter(applicationSetInfo *argoprojiov1alpha1.ApplicationSet) time.Duration {
var res time.Duration
for _, requestedGenerator := range applicationSetInfo.Spec.Generators {
relevantGenerators := generators.GetRelevantGenerators(&requestedGenerator, r.Generators)
for _, g := range relevantGenerators {
t := g.GetRequeueAfter(&requestedGenerator)
if res == 0 {
res = t
} else if t != 0 && t < res {
res = t
}
}
}
return res
}
func getTempApplication(applicationSetTemplate argoprojiov1alpha1.ApplicationSetTemplate) *argov1alpha1.Application {
var tmplApplication argov1alpha1.Application
tmplApplication.Annotations = applicationSetTemplate.Annotations
tmplApplication.Labels = applicationSetTemplate.Labels
tmplApplication.Namespace = applicationSetTemplate.Namespace
tmplApplication.Name = applicationSetTemplate.Name
tmplApplication.Spec = applicationSetTemplate.Spec
tmplApplication.Finalizers = applicationSetTemplate.Finalizers
return &tmplApplication
}
func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argoprojiov1alpha1.ApplicationSet) ([]argov1alpha1.Application, argoprojiov1alpha1.ApplicationSetReasonType, error) {
var res []argov1alpha1.Application
var firstError error
var applicationSetReason argoprojiov1alpha1.ApplicationSetReasonType
for _, requestedGenerator := range applicationSetInfo.Spec.Generators {
t, err := generators.Transform(requestedGenerator, r.Generators, applicationSetInfo.Spec.Template, &applicationSetInfo)
if err != nil {
log.WithError(err).WithField("generator", requestedGenerator).
Error("error generating application from params")
if firstError == nil {
firstError = err
applicationSetReason = argoprojiov1alpha1.ApplicationSetReasonApplicationParamsGenerationError
}
continue
}
for _, a := range t {
tmplApplication := getTempApplication(a.Template)
for _, p := range a.Params {
app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p)
if err != nil {
log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
Error("error generating application from params")
if firstError == nil {
firstError = err
applicationSetReason = argoprojiov1alpha1.ApplicationSetReasonRenderTemplateParamsError
}
continue
}
res = append(res, *app)
}
}
log.WithField("generator", requestedGenerator).Infof("generated %d applications", len(res))
log.WithField("generator", requestedGenerator).Debugf("apps from generator: %+v", res)
}
return res, applicationSetReason, firstError
}
func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &argov1alpha1.Application{}, ".metadata.controller", func(rawObj client.Object) []string {
// grab the job object, extract the owner...
app := rawObj.(*argov1alpha1.Application)
owner := metav1.GetControllerOf(app)
if owner == nil {
return nil
}
// ...make sure it's a application set...
if owner.APIVersion != argoprojiov1alpha1.GroupVersion.String() || owner.Kind != "ApplicationSet" {
return nil
}
// ...and if so, return it
return []string{owner.Name}
}); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&argoprojiov1alpha1.ApplicationSet{}).
Owns(&argov1alpha1.Application{}).
Watches(
&source.Kind{Type: &corev1.Secret{}},
&clusterSecretEventHandler{
Client: mgr.GetClient(),
Log: log.WithField("type", "createSecretEventHandler"),
}).
// TODO: also watch Applications and respond on changes if we own them.
Complete(r)
}
// createOrUpdateInCluster will create / update application resources in the cluster.
// - For new applications, it will call create
// - For existing application, it will call update
// The function also adds owner reference to all applications, and uses it to delete them.
func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context, applicationSet argoprojiov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error {
var firstError error
// Creates or updates the application in appList
for _, generatedApp := range desiredApplications {
appLog := log.WithFields(log.Fields{"app": generatedApp.Name, "appSet": applicationSet.Name})
generatedApp.Namespace = applicationSet.Namespace
found := &argov1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: generatedApp.Name,
Namespace: generatedApp.Namespace,
},
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
}
action, err := utils.CreateOrUpdate(ctx, r.Client, found, func() error {
// Copy only the Application/ObjectMeta fields that are significant, from the generatedApp
found.Spec = generatedApp.Spec
// Preserve argo cd notifications state (https://github.com/argoproj/applicationset/issues/180)
if state, exists := found.ObjectMeta.Annotations[NotifiedAnnotationKey]; exists {
if generatedApp.Annotations == nil {
generatedApp.Annotations = map[string]string{}
}
generatedApp.Annotations[NotifiedAnnotationKey] = state
}
found.ObjectMeta.Annotations = generatedApp.Annotations
found.ObjectMeta.Finalizers = generatedApp.Finalizers
found.ObjectMeta.Labels = generatedApp.Labels
return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme)
})
if err != nil {
appLog.WithError(err).WithField("action", action).Errorf("failed to %s Application", action)
if firstError == nil {
firstError = err
}
continue
}
r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, fmt.Sprint(action), "%s Application %q", action, generatedApp.Name)
appLog.Logf(log.InfoLevel, "%s Application", action)
}
return firstError
}
// createInCluster will filter from the desiredApplications only the application that needs to be created
// Then it will call createOrUpdateInCluster to do the actual create
func (r *ApplicationSetReconciler) createInCluster(ctx context.Context, applicationSet argoprojiov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error {
var createApps []argov1alpha1.Application
current, err := r.getCurrentApplications(ctx, applicationSet)
if err != nil {
return err
}
m := make(map[string]bool) // Will holds the app names that are current in the cluster
for _, app := range current {
m[app.Name] = true
}
// filter applications that are not in m[string]bool (new to the cluster)
for _, app := range desiredApplications {
_, exists := m[app.Name]
if !exists {
createApps = append(createApps, app)
}
}
return r.createOrUpdateInCluster(ctx, applicationSet, createApps)
}
func (r *ApplicationSetReconciler) getCurrentApplications(_ context.Context, applicationSet argoprojiov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) {
// TODO: Should this use the context param?
var current argov1alpha1.ApplicationList
err := r.Client.List(context.Background(), &current, client.MatchingFields{".metadata.controller": applicationSet.Name})
if err != nil {
return nil, err
}
return current.Items, nil
}
// deleteInCluster will delete Applications that are currently on the cluster, but not in appList.
// The function must be called after all generators had been called and generated applications
func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, applicationSet argoprojiov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error {
// settingsMgr := settings.NewSettingsManager(context.TODO(), r.KubeClientset, applicationSet.Namespace)
// argoDB := db.NewDB(applicationSet.Namespace, settingsMgr, r.KubeClientset)
// clusterList, err := argoDB.ListClusters(ctx)
clusterList, err := utils.ListClusters(ctx, r.KubeClientset, applicationSet.Namespace)
if err != nil {
return err
}
// Save current applications to be able to delete the ones that are not in appList
current, err := r.getCurrentApplications(ctx, applicationSet)
if err != nil {
return err
}
m := make(map[string]bool) // Will holds the app names in appList for the deletion process
for _, app := range desiredApplications {
m[app.Name] = true
}
// Delete apps that are not in m[string]bool
var firstError error
for _, app := range current {
appLog := log.WithFields(log.Fields{"app": app.Name, "appSet": applicationSet.Name})
_, exists := m[app.Name]
if !exists {
// Removes the Argo CD resources finalizer if the application contains an invalid target (eg missing cluster)
err := r.removeFinalizerOnInvalidDestination(ctx, applicationSet, &app, clusterList, appLog)
if err != nil {
appLog.WithError(err).Error("failed to update Application")
if firstError != nil {
firstError = err
}
continue
}
err = r.Client.Delete(ctx, &app)
if err != nil {
appLog.WithError(err).Error("failed to delete Application")
if firstError != nil {
firstError = err
}
continue
}
r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, "Deleted", "Deleted Application %q", app.Name)
appLog.Log(log.InfoLevel, "Deleted application")
}
}
return firstError
}
// removeFinalizerOnInvalidDestination removes the Argo CD resources finalizer if the application contains an invalid target (eg missing cluster)
func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx context.Context, applicationSet argoprojiov1alpha1.ApplicationSet, app *argov1alpha1.Application, clusterList *argov1alpha1.ClusterList, appLog *log.Entry) error {
// Only check if the finalizers need to be removed IF there are finalizers to remove
if len(app.Finalizers) == 0 {
return nil
}
var validDestination bool
// Detect if the destination is invalid (name doesn't correspond to a matching cluster)
if err := utils.ValidateDestination(ctx, &app.Spec.Destination, r.KubeClientset, applicationSet.Namespace); err != nil {
appLog.Warnf("The destination cluster for %s couldn't be found: %v", app.Name, err)
validDestination = false
} else {
// Detect if the destination's server field does not match an existing cluster
matchingCluster := false
for _, cluster := range clusterList.Items {
// Server fields must match. Note that ValidateDestination ensures that the server field is set, if applicable.
if app.Spec.Destination.Server != cluster.Server {
continue
}
// The name must match, if it is not empty
if app.Spec.Destination.Name != "" && cluster.Name != app.Spec.Destination.Name {
continue
}
matchingCluster = true
break
}
if !matchingCluster {
appLog.Warnf("A match for the destination cluster for %s, by server url, couldn't be found.", app.Name)
}
validDestination = matchingCluster
}
// If the destination is invalid (for example the cluster is no longer defined), then remove
// the application finalizers to avoid triggering Argo CD bug #5817
if !validDestination {
// Filter out the Argo CD finalizer from the finalizer list
var newFinalizers []string
for _, existingFinalizer := range app.Finalizers {
if existingFinalizer != argov1alpha1.ResourcesFinalizerName { // only remove this one
newFinalizers = append(newFinalizers, existingFinalizer)
}
}
// If the finalizer length changed (due to filtering out an Argo finalizer), update the finalizer list on the app
if len(newFinalizers) != len(app.Finalizers) {
app.Finalizers = newFinalizers
r.Recorder.Eventf(&applicationSet, corev1.EventTypeNormal, "Updated", "Updated Application %q finalizer before deletion, because application has an invalid destination", app.Name)
appLog.Log(log.InfoLevel, "Updating application finalizer before deletion, because application has an invalid destination")
err := r.Client.Update(ctx, app, &client.UpdateOptions{})
if err != nil {
return err
}
}
}
return nil
}
var _ handler.EventHandler = &clusterSecretEventHandler{}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
package controllers
import (
"context"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"github.com/argoproj/argo-cd/v2/applicationset/generators"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
// clusterSecretEventHandler is used when watching Secrets to check if they are ArgoCD Cluster Secrets, and if so
// requeue any related ApplicationSets.
type clusterSecretEventHandler struct {
//handler.EnqueueRequestForOwner
Log log.FieldLogger
Client client.Client
}
func (h *clusterSecretEventHandler) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) {
h.queueRelatedAppGenerators(q, e.Object)
}
func (h *clusterSecretEventHandler) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) {
h.queueRelatedAppGenerators(q, e.ObjectNew)
}
func (h *clusterSecretEventHandler) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) {
h.queueRelatedAppGenerators(q, e.Object)
}
func (h *clusterSecretEventHandler) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) {
h.queueRelatedAppGenerators(q, e.Object)
}
// addRateLimitingInterface defines the Add method of workqueue.RateLimitingInterface, allow us to easily mock
// it for testing purposes.
type addRateLimitingInterface interface {
Add(item interface{})
}
func (h *clusterSecretEventHandler) queueRelatedAppGenerators(q addRateLimitingInterface, object client.Object) {
// Check for label, lookup all ApplicationSets that might match the cluster, queue them all
if object.GetLabels()[generators.ArgoCDSecretTypeLabel] != generators.ArgoCDSecretTypeCluster {
return
}
h.Log.WithFields(log.Fields{
"namespace": object.GetNamespace(),
"name": object.GetName(),
}).Info("processing event for cluster secret")
appSetList := &argoprojiov1alpha1.ApplicationSetList{}
err := h.Client.List(context.Background(), appSetList)
if err != nil {
h.Log.WithError(err).Error("unable to list ApplicationSets")
return
}
h.Log.WithField("count", len(appSetList.Items)).Info("listed ApplicationSets")
for _, appSet := range appSetList.Items {
foundClusterGenerator := false
for _, generator := range appSet.Spec.Generators {
if generator.Clusters != nil {
foundClusterGenerator = true
break
}
}
if foundClusterGenerator {
// TODO: only queue the AppGenerator if the labels match this cluster
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: appSet.Namespace, Name: appSet.Name}}
q.Add(req)
}
}
}

View file

@ -0,0 +1,234 @@
package controllers
import (
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/argoproj/argo-cd/v2/applicationset/generators"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestClusterEventHandler(t *testing.T) {
scheme := runtime.NewScheme()
err := argoprojiov1alpha1.AddToScheme(scheme)
assert.Nil(t, err)
err = argov1alpha1.AddToScheme(scheme)
assert.Nil(t, err)
tests := []struct {
name string
items []argoprojiov1alpha1.ApplicationSet
secret corev1.Secret
expectedRequests []ctrl.Request
}{
{
name: "no application sets should mean no requests",
items: []argoprojiov1alpha1.ApplicationSet{},
secret: corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
expectedRequests: []reconcile.Request{},
},
{
name: "a cluster generator should produce a request",
items: []argoprojiov1alpha1.ApplicationSet{
{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
Clusters: &argoprojiov1alpha1.ClusterGenerator{},
},
},
},
},
},
secret: corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
expectedRequests: []reconcile.Request{{
NamespacedName: types.NamespacedName{Namespace: "argocd", Name: "my-app-set"},
}},
},
{
name: "multiple cluster generators should produce multiple requests",
items: []argoprojiov1alpha1.ApplicationSet{
{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
Clusters: &argoprojiov1alpha1.ClusterGenerator{},
},
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set2",
Namespace: "argocd",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
Clusters: &argoprojiov1alpha1.ClusterGenerator{},
},
},
},
},
},
secret: corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
expectedRequests: []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: "argocd", Name: "my-app-set"}},
{NamespacedName: types.NamespacedName{Namespace: "argocd", Name: "my-app-set2"}},
},
},
{
name: "non-cluster generator should not match",
items: []argoprojiov1alpha1.ApplicationSet{
{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "another-namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
Clusters: &argoprojiov1alpha1.ClusterGenerator{},
},
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Name: "app-set-non-cluster",
Namespace: "argocd",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
List: &argoprojiov1alpha1.ListGenerator{},
},
},
},
},
},
secret: corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
expectedRequests: []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: "another-namespace", Name: "my-app-set"}},
},
},
{
name: "non-argo cd secret should not match",
items: []argoprojiov1alpha1.ApplicationSet{
{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "another-namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
Clusters: &argoprojiov1alpha1.ClusterGenerator{},
},
},
},
},
},
secret: corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-non-argocd-secret",
},
},
expectedRequests: []reconcile.Request{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
appSetList := argoprojiov1alpha1.ApplicationSetList{
Items: test.items,
}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(&appSetList).Build()
handler := &clusterSecretEventHandler{
Client: fakeClient,
Log: log.WithField("type", "createSecretEventHandler"),
}
mockAddRateLimitingInterface := mockAddRateLimitingInterface{}
handler.queueRelatedAppGenerators(&mockAddRateLimitingInterface, &test.secret)
assert.False(t, mockAddRateLimitingInterface.errorOccurred)
assert.ElementsMatch(t, mockAddRateLimitingInterface.addedItems, test.expectedRequests)
})
}
}
// Add checks the type, and adds it to the internal list of received additions
func (obj *mockAddRateLimitingInterface) Add(item interface{}) {
if req, ok := item.(ctrl.Request); ok {
obj.addedItems = append(obj.addedItems, req)
} else {
obj.errorOccurred = true
}
}
type mockAddRateLimitingInterface struct {
errorOccurred bool
addedItems []ctrl.Request
}

View file

@ -0,0 +1,184 @@
package generators
import (
"context"
"fmt"
"regexp"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v2/util/settings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoappsetv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
const (
ArgoCDSecretTypeLabel = "argocd.argoproj.io/secret-type"
ArgoCDSecretTypeCluster = "cluster"
)
var _ Generator = (*ClusterGenerator)(nil)
// ClusterGenerator generates Applications for some or all clusters registered with ArgoCD.
type ClusterGenerator struct {
client.Client
ctx context.Context
clientset kubernetes.Interface
// namespace is the Argo CD namespace
namespace string
settingsManager *settings.SettingsManager
}
func NewClusterGenerator(c client.Client, ctx context.Context, clientset kubernetes.Interface, namespace string) Generator {
settingsManager := settings.NewSettingsManager(ctx, clientset, namespace)
g := &ClusterGenerator{
Client: c,
ctx: ctx,
clientset: clientset,
namespace: namespace,
settingsManager: settingsManager,
}
return g
}
func (g *ClusterGenerator) GetRequeueAfter(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) time.Duration {
return NoRequeueAfter
}
func (g *ClusterGenerator) GetTemplate(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) *argoappsetv1alpha1.ApplicationSetTemplate {
return &appSetGenerator.Clusters.Template
}
func (g *ClusterGenerator) GenerateParams(
appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator, _ *argoappsetv1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator == nil {
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.Clusters == nil {
return nil, EmptyAppSetGeneratorError
}
// Do not include the local cluster in the cluster parameters IF there is a non-empty selector
// - Since local clusters do not have secrets, they do not have labels to match against
ignoreLocalClusters := len(appSetGenerator.Clusters.Selector.MatchExpressions) > 0 || len(appSetGenerator.Clusters.Selector.MatchLabels) > 0
// ListCluster from Argo CD's util/db package will include the local cluster in the list of clusters
clustersFromArgoCD, err := utils.ListClusters(g.ctx, g.clientset, g.namespace)
if err != nil {
return nil, err
}
if clustersFromArgoCD == nil {
return nil, nil
}
clusterSecrets, err := g.getSecretsByClusterName(appSetGenerator)
if err != nil {
return nil, err
}
res := []map[string]string{}
secretsFound := []corev1.Secret{}
for _, cluster := range clustersFromArgoCD.Items {
// If there is a secret for this cluster, then it's a non-local cluster, so it will be
// handled by the next step.
if secretForCluster, exists := clusterSecrets[cluster.Name]; exists {
secretsFound = append(secretsFound, secretForCluster)
} else if !ignoreLocalClusters {
// If there is no secret for the cluster, it's the local cluster, so handle it here.
params := map[string]string{}
params["name"] = cluster.Name
params["server"] = cluster.Server
for key, value := range appSetGenerator.Clusters.Values {
params[fmt.Sprintf("values.%s", key)] = value
}
log.WithField("cluster", "local cluster").Info("matched local cluster")
res = append(res, params)
}
}
// For each matching cluster secret (non-local clusters only)
for _, cluster := range secretsFound {
params := map[string]string{}
params["name"] = string(cluster.Data["name"])
params["nameNormalized"] = sanitizeName(string(cluster.Data["name"]))
params["server"] = string(cluster.Data["server"])
for key, value := range cluster.ObjectMeta.Annotations {
params[fmt.Sprintf("metadata.annotations.%s", key)] = value
}
for key, value := range cluster.ObjectMeta.Labels {
params[fmt.Sprintf("metadata.labels.%s", key)] = value
}
for key, value := range appSetGenerator.Clusters.Values {
params[fmt.Sprintf("values.%s", key)] = value
}
log.WithField("cluster", cluster.Name).Info("matched cluster secret")
res = append(res, params)
}
return res, nil
}
func (g *ClusterGenerator) getSecretsByClusterName(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) (map[string]corev1.Secret, error) {
// List all Clusters:
clusterSecretList := &corev1.SecretList{}
selector := metav1.AddLabelToSelector(&appSetGenerator.Clusters.Selector, ArgoCDSecretTypeLabel, ArgoCDSecretTypeCluster)
secretSelector, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return nil, err
}
if err := g.Client.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
return nil, err
}
log.Debug("clusters matching labels", "count", len(clusterSecretList.Items))
res := map[string]corev1.Secret{}
for _, cluster := range clusterSecretList.Items {
clusterName := string(cluster.Data["name"])
res[clusterName] = cluster
}
return res, nil
}
// santize the name in accordance with the below rules
// 1. contain no more than 253 characters
// 2. contain only lowercase alphanumeric characters, '-' or '.'
// 3. start and end with an alphanumeric character
func sanitizeName(name string) string {
invalidDNSNameChars := regexp.MustCompile("[^-a-z0-9.]")
maxDNSNameLength := 253
name = strings.ToLower(name)
name = invalidDNSNameChars.ReplaceAllString(name, "-")
if len(name) > maxDNSNameLength {
name = name[:maxDNSNameLength]
}
return strings.Trim(name, "-.")
}

View file

@ -0,0 +1,254 @@
package generators
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"testing"
kubefake "k8s.io/client-go/kubernetes/fake"
argoappsetv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"github.com/stretchr/testify/assert"
)
type possiblyErroringFakeCtrlRuntimeClient struct {
client.Client
shouldError bool
}
func (p *possiblyErroringFakeCtrlRuntimeClient) List(ctx context.Context, secretList client.ObjectList, opts ...client.ListOption) error {
if p.shouldError {
return fmt.Errorf("could not list Secrets")
}
return p.Client.List(ctx, secretList, opts...)
}
func TestGenerateParams(t *testing.T) {
clusters := []client.Object{
&corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "staging-01",
Namespace: "namespace",
Labels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
"org": "foo",
},
Annotations: map[string]string{
"foo.argoproj.io": "staging",
},
},
Data: map[string][]byte{
"config": []byte("{}"),
"name": []byte("staging-01"),
"server": []byte("https://staging-01.example.com"),
},
Type: corev1.SecretType("Opaque"),
},
&corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "production-01",
Namespace: "namespace",
Labels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
Annotations: map[string]string{
"foo.argoproj.io": "production",
},
},
Data: map[string][]byte{
"config": []byte("{}"),
"name": []byte("production_01/west"),
"server": []byte("https://production-01.example.com"),
},
Type: corev1.SecretType("Opaque"),
},
}
testCases := []struct {
name string
selector metav1.LabelSelector
values map[string]string
expected []map[string]string
// clientError is true if a k8s client error should be simulated
clientError bool
expectedError error
}{
{
name: "no label selector",
selector: metav1.LabelSelector{},
values: nil,
expected: []map[string]string{
{"name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"},
{"name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"},
{"name": "in-cluster", "server": "https://kubernetes.default.svc"},
},
clientError: false,
expectedError: nil,
},
{
name: "secret type label selector",
selector: metav1.LabelSelector{
MatchLabels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
},
},
values: nil,
expected: []map[string]string{
{"name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"},
{"name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"},
},
clientError: false,
expectedError: nil,
},
{
name: "production-only",
selector: metav1.LabelSelector{
MatchLabels: map[string]string{
"environment": "production",
},
},
values: map[string]string{
"foo": "bar",
},
expected: []map[string]string{
{"values.foo": "bar", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"},
},
clientError: false,
expectedError: nil,
},
{
name: "production or staging",
selector: metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "environment",
Operator: "In",
Values: []string{
"production",
"staging",
},
},
},
},
values: map[string]string{
"foo": "bar",
},
expected: []map[string]string{
{"values.foo": "bar", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"},
{"values.foo": "bar", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production"},
},
clientError: false,
expectedError: nil,
},
{
name: "production or staging with match labels",
selector: metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "environment",
Operator: "In",
Values: []string{
"production",
"staging",
},
},
},
MatchLabels: map[string]string{
"org": "foo",
},
},
values: map[string]string{
"name": "baz",
},
expected: []map[string]string{
{"values.name": "baz", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging"},
},
clientError: false,
expectedError: nil,
},
{
name: "simulate client error",
selector: metav1.LabelSelector{},
values: nil,
expected: nil,
clientError: true,
expectedError: fmt.Errorf("could not list Secrets"),
},
}
// convert []client.Object to []runtime.Object, for use by kubefake package
runtimeClusters := []runtime.Object{}
for _, clientCluster := range clusters {
runtimeClusters = append(runtimeClusters, clientCluster)
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
cl := &possiblyErroringFakeCtrlRuntimeClient{
fakeClient,
testCase.clientError,
}
var clusterGenerator = NewClusterGenerator(cl, context.Background(), appClientset, "namespace")
got, err := clusterGenerator.GenerateParams(&argoappsetv1alpha1.ApplicationSetGenerator{
Clusters: &argoappsetv1alpha1.ClusterGenerator{
Selector: testCase.selector,
Values: testCase.values,
},
}, nil)
if testCase.expectedError != nil {
assert.EqualError(t, err, testCase.expectedError.Error())
} else {
assert.NoError(t, err)
assert.ElementsMatch(t, testCase.expected, got)
}
})
}
}
func TestSanitizeClusterName(t *testing.T) {
t.Run("valid DNS-1123 subdomain name", func(t *testing.T) {
assert.Equal(t, "cluster-name", sanitizeName("cluster-name"))
})
t.Run("invalid DNS-1123 subdomain name", func(t *testing.T) {
invalidName := "-.--CLUSTER/name -./.-"
assert.Equal(t, "cluster-name", sanitizeName(invalidName))
})
}

View file

@ -0,0 +1,229 @@
package generators
import (
"context"
"fmt"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v2/util/settings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
var _ Generator = (*DuckTypeGenerator)(nil)
// DuckTypeGenerator generates Applications for some or all clusters registered with ArgoCD.
type DuckTypeGenerator struct {
ctx context.Context
dynClient dynamic.Interface
clientset kubernetes.Interface
namespace string // namespace is the Argo CD namespace
settingsManager *settings.SettingsManager
}
func NewDuckTypeGenerator(ctx context.Context, dynClient dynamic.Interface, clientset kubernetes.Interface, namespace string) Generator {
settingsManager := settings.NewSettingsManager(ctx, clientset, namespace)
g := &DuckTypeGenerator{
ctx: ctx,
dynClient: dynClient,
clientset: clientset,
namespace: namespace,
settingsManager: settingsManager,
}
return g
}
func (g *DuckTypeGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
// Return a requeue default of 3 minutes, if no override is specified.
if appSetGenerator.ClusterDecisionResource.RequeueAfterSeconds != nil {
return time.Duration(*appSetGenerator.ClusterDecisionResource.RequeueAfterSeconds) * time.Second
}
return DefaultRequeueAfterSeconds
}
func (g *DuckTypeGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.ClusterDecisionResource.Template
}
func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, _ *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator == nil {
return nil, EmptyAppSetGeneratorError
}
// Not likely to happen
if appSetGenerator.ClusterDecisionResource == nil {
return nil, EmptyAppSetGeneratorError
}
// ListCluster from Argo CD's util/db package will include the local cluster in the list of clusters
clustersFromArgoCD, err := utils.ListClusters(g.ctx, g.clientset, g.namespace)
if err != nil {
return nil, err
}
if clustersFromArgoCD == nil {
return nil, nil
}
// Read the configMapRef
cm, err := g.clientset.CoreV1().ConfigMaps(g.namespace).Get(g.ctx, appSetGenerator.ClusterDecisionResource.ConfigMapRef, metav1.GetOptions{})
if err != nil {
return nil, err
}
// Extract GVK data for the dynamic client to use
versionIdx := strings.Index(cm.Data["apiVersion"], "/")
kind := cm.Data["kind"]
resourceName := appSetGenerator.ClusterDecisionResource.Name
labelSelector := appSetGenerator.ClusterDecisionResource.LabelSelector
log.WithField("kind.apiVersion", kind+"."+cm.Data["apiVersion"]).Info("Kind.Group/Version Reference")
// Validate the fields
if kind == "" || versionIdx < 1 {
log.Warningf("kind=%v, resourceName=%v, versionIdx=%v", kind, resourceName, versionIdx)
return nil, fmt.Errorf("There is a problem with the apiVersion, kind or resourceName provided")
}
if (resourceName == "" && labelSelector.MatchLabels == nil && labelSelector.MatchExpressions == nil) ||
(resourceName != "" && (labelSelector.MatchExpressions != nil || labelSelector.MatchLabels != nil)) {
log.Warningf("You must choose either resourceName=%v, labelSelector.matchLabels=%v or labelSelect.matchExpressions=%v", resourceName, labelSelector.MatchLabels, labelSelector.MatchExpressions)
return nil, fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator")
}
// Split up the apiVersion
group := cm.Data["apiVersion"][0:versionIdx]
version := cm.Data["apiVersion"][versionIdx+1:]
log.WithField("kind.group.version", kind+"."+group+"/"+version).Debug("decoded Ref")
duckGVR := schema.GroupVersionResource{Group: group, Version: version, Resource: kind}
listOptions := metav1.ListOptions{}
if resourceName == "" {
listOptions.LabelSelector = metav1.FormatLabelSelector(&labelSelector)
log.WithField("listOptions.LabelSelector", listOptions.LabelSelector).Info("selection type")
} else {
listOptions.FieldSelector = fields.OneTermEqualSelector("metadata.name", resourceName).String()
//metav1.Convert_fields_Selector_To_string(fields.).Sprintf("metadata.name=%s", resourceName)
log.WithField("listOptions.FieldSelector", listOptions.FieldSelector).Info("selection type")
}
duckResources, err := g.dynClient.Resource(duckGVR).Namespace(g.namespace).List(g.ctx, listOptions)
if err != nil {
log.WithField("GVK", duckGVR).Warning("resources were not found")
return nil, err
}
if len(duckResources.Items) == 0 {
log.Warning("no resource found, make sure you clusterDecisionResource is defined correctly")
return nil, fmt.Errorf("no clusterDecisionResources found")
}
// Override the duck type in the status of the resource
statusListKey := "clusters"
matchKey := cm.Data["matchKey"]
if cm.Data["statusListKey"] != "" {
statusListKey = cm.Data["statusListKey"]
}
if matchKey == "" {
log.WithField("matchKey", matchKey).Warning("matchKey not found in " + cm.Name)
return nil, nil
}
res := []map[string]string{}
clusterDecisions := []interface{}{}
// Build the decision slice
for _, duckResource := range duckResources.Items {
log.WithField("duckResourceName", duckResource.GetName()).Debug("found resource")
if duckResource.Object["status"] == nil || len(duckResource.Object["status"].(map[string]interface{})) == 0 {
log.Warningf("clusterDecisionResource: %s, has no status", duckResource.GetName())
continue
}
log.WithField("duckResourceStatus", duckResource.Object["status"]).Debug("found resource")
clusterDecisions = append(clusterDecisions, duckResource.Object["status"].(map[string]interface{})[statusListKey].([]interface{})...)
}
log.Infof("Number of decisions found: %v", len(clusterDecisions))
// Read this outside the loop to improve performance
argoClusters := clustersFromArgoCD.Items
if len(clusterDecisions) > 0 {
for _, cluster := range clusterDecisions {
// generated instance of cluster params
params := map[string]string{}
log.Infof("cluster: %v", cluster)
matchValue := cluster.(map[string]interface{})[matchKey]
if matchValue == nil || matchValue.(string) == "" {
log.Warningf("matchKey=%v not found in \"%v\" list: %v\n", matchKey, statusListKey, cluster.(map[string]interface{}))
continue
}
strMatchValue := matchValue.(string)
log.WithField(matchKey, strMatchValue).Debug("validate against ArgoCD")
found := false
for _, argoCluster := range argoClusters {
if argoCluster.Name == strMatchValue {
log.WithField(matchKey, argoCluster.Name).Info("matched cluster in ArgoCD")
params["name"] = argoCluster.Name
params["server"] = argoCluster.Server
found = true
break // Stop looking
}
}
if !found {
log.WithField(matchKey, strMatchValue).Warning("unmatched cluster in ArgoCD")
continue
}
for key, value := range cluster.(map[string]interface{}) {
params[key] = value.(string)
}
for key, value := range appSetGenerator.ClusterDecisionResource.Values {
params[fmt.Sprintf("values.%s", key)] = value
}
res = append(res, params)
}
} else {
log.Warningf("clusterDecisionResource status." + statusListKey + " missing")
return nil, nil
}
return res, nil
}

View file

@ -0,0 +1,315 @@
package generators
import (
"context"
"fmt"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
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"
dynfake "k8s.io/client-go/dynamic/fake"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"testing"
)
const resourceApiVersion = "mallard.io/v1"
const resourceKind = "ducks"
const resourceName = "quak"
func TestGenerateParamsForDuckType(t *testing.T) {
clusters := []client.Object{
&corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "staging-01",
Namespace: "namespace",
Labels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
"org": "foo",
},
Annotations: map[string]string{
"foo.argoproj.io": "staging",
},
},
Data: map[string][]byte{
"config": []byte("{}"),
"name": []byte("staging-01"),
"server": []byte("https://staging-01.example.com"),
},
Type: corev1.SecretType("Opaque"),
},
&corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "production-01",
Namespace: "namespace",
Labels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
Annotations: map[string]string{
"foo.argoproj.io": "production",
},
},
Data: map[string][]byte{
"config": []byte("{}"),
"name": []byte("production-01"),
"server": []byte("https://production-01.example.com"),
},
Type: corev1.SecretType("Opaque"),
},
}
duckType := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]interface{}{"duck": "all-species"},
},
"status": map[string]interface{}{
"decisions": []interface{}{
map[string]interface{}{
"clusterName": "staging-01",
},
map[string]interface{}{
"clusterName": "production-01",
},
},
},
},
}
duckTypeProdOnly := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]interface{}{"duck": "spotted"},
},
"status": map[string]interface{}{
"decisions": []interface{}{
map[string]interface{}{
"clusterName": "production-01",
},
},
},
},
}
duckTypeEmpty := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]interface{}{"duck": "canvasback"},
},
"status": map[string]interface{}{},
},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "my-configmap",
Namespace: "namespace",
},
Data: map[string]string{
"apiVersion": resourceApiVersion,
"kind": resourceKind,
"statusListKey": "decisions",
"matchKey": "clusterName",
},
}
testCases := []struct {
name string
configMapRef string
resourceName string
labelSelector metav1.LabelSelector
resource *unstructured.Unstructured
values map[string]string
expected []map[string]string
expectedError error
}{
{
name: "no duck resource",
resourceName: "",
resource: duckType,
values: nil,
expected: []map[string]string{},
expectedError: fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator"),
},
/*** This does not work with the FAKE runtime client, fieldSelectors are broken.
{
name: "invalid name for duck resource",
resourceName: resourceName + "-different",
resource: duckType,
values: nil,
expected: []map[string]string{},
expectedError: fmt.Errorf("duck.mallard.io \"quak\" not found"),
},
***/
{
name: "duck type generator resourceName",
resourceName: resourceName,
resource: duckType,
values: nil,
expected: []map[string]string{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
},
expectedError: nil,
},
{
name: "production-only",
resourceName: resourceName,
resource: duckTypeProdOnly,
values: map[string]string{
"foo": "bar",
},
expected: []map[string]string{
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
},
expectedError: nil,
},
{
name: "duck type empty status",
resourceName: resourceName,
resource: duckTypeEmpty,
values: nil,
expected: nil,
expectedError: nil,
},
{
name: "duck type empty status labelSelector.matchLabels",
resourceName: "",
labelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"duck": "canvasback"}},
resource: duckTypeEmpty,
values: nil,
expected: nil,
expectedError: nil,
},
{
name: "duck type generator labelSelector.matchLabels",
resourceName: "",
labelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"duck": "all-species"}},
resource: duckType,
values: nil,
expected: []map[string]string{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
},
expectedError: nil,
},
{
name: "production-only labelSelector.matchLabels",
resourceName: "",
resource: duckTypeProdOnly,
labelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"duck": "spotted"}},
values: map[string]string{
"foo": "bar",
},
expected: []map[string]string{
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
},
expectedError: nil,
},
{
name: "duck type generator labelSelector.matchExpressions",
resourceName: "",
labelSelector: metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "duck",
Operator: "In",
Values: []string{"all-species", "marbled"},
},
}},
resource: duckType,
values: nil,
expected: []map[string]string{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
},
expectedError: nil,
},
{
name: "duck type generator resourceName and labelSelector.matchExpressions",
resourceName: resourceName,
labelSelector: metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "duck",
Operator: "In",
Values: []string{"all-species", "marbled"},
},
}},
resource: duckType,
values: nil,
expected: nil,
expectedError: fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator"),
},
}
// convert []client.Object to []runtime.Object, for use by kubefake package
runtimeClusters := []runtime.Object{}
for _, clientCluster := range clusters {
runtimeClusters = append(runtimeClusters, clientCluster)
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
appClientset := kubefake.NewSimpleClientset(append(runtimeClusters, configMap)...)
gvrToListKind := map[schema.GroupVersionResource]string{{
Group: "mallard.io",
Version: "v1",
Resource: "ducks",
}: "DuckList"}
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, testCase.resource)
var duckTypeGenerator = NewDuckTypeGenerator(context.Background(), fakeDynClient, appClientset, "namespace")
got, err := duckTypeGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
ClusterDecisionResource: &argoprojiov1alpha1.DuckTypeGenerator{
ConfigMapRef: "my-configmap",
Name: testCase.resourceName,
LabelSelector: testCase.labelSelector,
Values: testCase.values,
},
}, nil)
if testCase.expectedError != nil {
assert.EqualError(t, err, testCase.expectedError.Error())
} else {
assert.NoError(t, err)
assert.ElementsMatch(t, testCase.expected, got)
}
})
}
}

View file

@ -0,0 +1,83 @@
package generators
import (
"reflect"
"github.com/imdario/mergo"
log "github.com/sirupsen/logrus"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
type TransformResult struct {
Params []map[string]string
Template argoprojiov1alpha1.ApplicationSetTemplate
}
//Transform a spec generator to list of paramSets and a template
func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, allGenerators map[string]Generator, baseTemplate argoprojiov1alpha1.ApplicationSetTemplate, appSet *argoprojiov1alpha1.ApplicationSet) ([]TransformResult, error) {
res := []TransformResult{}
var firstError error
generators := GetRelevantGenerators(&requestedGenerator, allGenerators)
for _, g := range generators {
// we call mergeGeneratorTemplate first because GenerateParams might be more costly so we want to fail fast if there is an error
mergedTemplate, err := mergeGeneratorTemplate(g, &requestedGenerator, baseTemplate)
if err != nil {
log.WithError(err).WithField("generator", g).
Error("error generating params")
if firstError == nil {
firstError = err
}
continue
}
params, err := g.GenerateParams(&requestedGenerator, appSet)
if err != nil {
log.WithError(err).WithField("generator", g).
Error("error generating params")
if firstError == nil {
firstError = err
}
continue
}
res = append(res, TransformResult{
Params: params,
Template: mergedTemplate,
})
}
return res, firstError
}
func GetRelevantGenerators(requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, generators map[string]Generator) []Generator {
var res []Generator
v := reflect.Indirect(reflect.ValueOf(requestedGenerator))
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.CanInterface() {
continue
}
if !reflect.ValueOf(field.Interface()).IsNil() {
res = append(res, generators[v.Type().Field(i).Name])
}
}
return res
}
func mergeGeneratorTemplate(g Generator, requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetTemplate argoprojiov1alpha1.ApplicationSetTemplate) (argoprojiov1alpha1.ApplicationSetTemplate, error) {
// Make a copy of the value from `GetTemplate()` before merge, rather than copying directly into
// the provided parameter (which will touch the original resource object returned by client-go)
dest := g.GetTemplate(requestedGenerator).DeepCopy()
err := mergo.Merge(dest, applicationSetTemplate)
return *dest, err
}

View file

@ -0,0 +1,225 @@
package generators
import (
"context"
"fmt"
"path"
"sort"
"strconv"
"strings"
"time"
"github.com/jeremywohl/flatten"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/yaml"
"github.com/argoproj/argo-cd/v2/applicationset/services"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
var _ Generator = (*GitGenerator)(nil)
type GitGenerator struct {
repos services.Repos
}
func NewGitGenerator(repos services.Repos) Generator {
g := &GitGenerator{
repos: repos,
}
return g
}
func (g *GitGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.Git.Template
}
func (g *GitGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
// Return a requeue default of 3 minutes, if no default is specified.
if appSetGenerator.Git.RequeueAfterSeconds != nil {
return time.Duration(*appSetGenerator.Git.RequeueAfterSeconds) * time.Second
}
return DefaultRequeueAfterSeconds
}
func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, _ *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator == nil {
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.Git == nil {
return nil, EmptyAppSetGeneratorError
}
var err error
var res []map[string]string
if appSetGenerator.Git.Directories != nil {
res, err = g.generateParamsForGitDirectories(appSetGenerator)
} else if appSetGenerator.Git.Files != nil {
res, err = g.generateParamsForGitFiles(appSetGenerator)
} else {
return nil, EmptyAppSetGeneratorError
}
if err != nil {
return nil, err
}
return res, nil
}
func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) ([]map[string]string, error) {
// Directories, not files
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision)
if err != nil {
return nil, err
}
log.WithFields(log.Fields{
"allPaths": allPaths,
"total": len(allPaths),
"repoURL": appSetGenerator.Git.RepoURL,
"revision": appSetGenerator.Git.Revision,
}).Info("applications result from the repo service")
requestedApps := g.filterApps(appSetGenerator.Git.Directories, allPaths)
res := g.generateParamsFromApps(requestedApps, appSetGenerator)
return res, nil
}
func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) ([]map[string]string, error) {
// Get all files that match the requested path string, removing duplicates
allFiles := make(map[string][]byte)
for _, requestedPath := range appSetGenerator.Git.Files {
files, err := g.repos.GetFiles(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, requestedPath.Path)
if err != nil {
return nil, err
}
for filePath, content := range files {
allFiles[filePath] = content
}
}
// Extract the unduplicated map into a list, and sort by path to ensure a deterministic
// processing order in the subsequent step
allPaths := []string{}
for path := range allFiles {
allPaths = append(allPaths, path)
}
sort.Strings(allPaths)
// Generate params from each path, and return
res := []map[string]string{}
for _, path := range allPaths {
// A JSON / YAML file path can contain multiple sets of parameters (ie it is an array)
paramsArray, err := g.generateParamsFromGitFile(path, allFiles[path])
if err != nil {
return nil, fmt.Errorf("unable to process file '%s': %v", path, err)
}
for index := range paramsArray {
res = append(res, paramsArray[index])
}
}
return res, nil
}
func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []byte) ([]map[string]string, error) {
objectsFound := []map[string]interface{}{}
// First, we attempt to parse as an array
err := yaml.Unmarshal(fileContent, &objectsFound)
if err != nil {
// If unable to parse as an array, attempt to parse as a single object
singleObj := make(map[string]interface{})
err = yaml.Unmarshal(fileContent, &singleObj)
if err != nil {
return nil, fmt.Errorf("unable to parse file: %v", err)
}
objectsFound = append(objectsFound, singleObj)
}
res := []map[string]string{}
// Flatten all objects found, and return them
for _, objectFound := range objectsFound {
flat, err := flatten.Flatten(objectFound, "", flatten.DotStyle)
if err != nil {
return nil, err
}
params := map[string]string{}
for k, v := range flat {
params[k] = fmt.Sprintf("%v", v)
}
params["path"] = path.Dir(filePath)
params["path.basename"] = path.Base(params["path"])
params["path.basenameNormalized"] = sanitizeName(path.Base(params["path"]))
for k, v := range strings.Split(strings.TrimSuffix(params["path"], params["path.basename"]), "/") {
if len(v) > 0 {
params["path["+strconv.Itoa(k)+"]"] = v
}
}
res = append(res, params)
}
return res, nil
}
func (g *GitGenerator) filterApps(Directories []argoprojiov1alpha1.GitDirectoryGeneratorItem, allPaths []string) []string {
res := []string{}
for _, appPath := range allPaths {
appInclude := false
appExclude := false
// Iterating over each appPath and check whether directories object has requestedPath that matches the appPath
for _, requestedPath := range Directories {
match, err := path.Match(requestedPath.Path, appPath)
if err != nil {
log.WithError(err).WithField("requestedPath", requestedPath).
WithField("appPath", appPath).Error("error while matching appPath to requestedPath")
continue
}
if match && !requestedPath.Exclude {
appInclude = true
}
if match && requestedPath.Exclude {
appExclude = true
}
}
// Whenever there is a path with exclude: true it wont be included, even if it is included in a different path pattern
if appInclude && !appExclude {
res = append(res, appPath)
}
}
return res
}
func (g *GitGenerator) generateParamsFromApps(requestedApps []string, _ *argoprojiov1alpha1.ApplicationSetGenerator) []map[string]string {
// TODO: At some point, the appicationSetGenerator param should be used
res := make([]map[string]string, len(requestedApps))
for i, a := range requestedApps {
params := make(map[string]string, 2)
params["path"] = a
params["path.basename"] = path.Base(a)
params["path.basenameNormalized"] = sanitizeName(path.Base(a))
for k, v := range strings.Split(strings.TrimSuffix(params["path"], params["path.basename"]), "/") {
if len(v) > 0 {
params["path["+strconv.Itoa(k)+"]"] = v
}
}
res[i] = params
}
return res
}

View file

@ -0,0 +1,452 @@
package generators
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
// type clientSet struct {
// RepoServerServiceClient apiclient.RepoServerServiceClient
// }
// func (c *clientSet) NewRepoServerClient() (io.Closer, apiclient.RepoServerServiceClient, error) {
// return io.NewCloser(func() error { return nil }), c.RepoServerServiceClient, nil
// }
type argoCDServiceMock struct {
mock *mock.Mock
}
func (a argoCDServiceMock) GetApps(ctx context.Context, repoURL string, revision string) ([]string, error) {
args := a.mock.Called(ctx, repoURL, revision)
return args.Get(0).([]string), args.Error(1)
}
func (a argoCDServiceMock) GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error) {
args := a.mock.Called(ctx, repoURL, revision, pattern)
return args.Get(0).(map[string][]byte), args.Error(1)
}
func (a argoCDServiceMock) GetFileContent(ctx context.Context, repoURL string, revision string, path string) ([]byte, error) {
args := a.mock.Called(ctx, repoURL, revision, path)
return args.Get(0).([]byte), args.Error(1)
}
func (a argoCDServiceMock) GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) {
args := a.mock.Called(ctx, repoURL, revision)
return args.Get(0).([]string), args.Error(1)
}
func TestGitGenerateParamsFromDirectories(t *testing.T) {
cases := []struct {
name string
directories []argoprojiov1alpha1.GitDirectoryGeneratorItem
repoApps []string
repoError error
expected []map[string]string
expectedError error
}{
{
name: "happy flow - created apps",
directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{
"app1",
"app2",
"app_3",
"p1/app4",
},
repoError: nil,
expected: []map[string]string{
{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1"},
{"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2"},
{"path": "app_3", "path.basename": "app_3", "path.basenameNormalized": "app-3"},
},
expectedError: nil,
},
{
name: "It filters application according to the paths",
directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "p1/*"}, {Path: "p1/*/*"}},
repoApps: []string{
"app1",
"p1/app2",
"p1/p2/app3",
"p1/p2/p3/app4",
},
repoError: nil,
expected: []map[string]string{
{"path": "p1/app2", "path.basename": "app2", "path[0]": "p1", "path.basenameNormalized": "app2"},
{"path": "p1/p2/app3", "path.basename": "app3", "path[0]": "p1", "path[1]": "p2", "path.basenameNormalized": "app3"},
},
expectedError: nil,
},
{
name: "It filters application according to the paths with Exclude",
directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "p1/*", Exclude: true}, {Path: "*"}, {Path: "*/*"}},
repoApps: []string{
"app1",
"app2",
"p1/app2",
"p1/app3",
"p2/app3",
},
repoError: nil,
expected: []map[string]string{
{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1"},
{"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2"},
{"path": "p2/app3", "path.basename": "app3", "path[0]": "p2", "path.basenameNormalized": "app3"},
},
expectedError: nil,
},
{
name: "Expecting same exclude behavior with different order",
directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}, {Path: "*/*"}, {Path: "p1/*", Exclude: true}},
repoApps: []string{
"app1",
"app2",
"p1/app2",
"p1/app3",
"p2/app3",
},
repoError: nil,
expected: []map[string]string{
{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1"},
{"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2"},
{"path": "p2/app3", "path.basename": "app3", "path[0]": "p2", "path.basenameNormalized": "app3"},
},
expectedError: nil,
},
{
name: "handles empty response from repo server",
directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{},
repoError: nil,
expected: []map[string]string{},
expectedError: nil,
},
{
name: "handles error from repo server",
directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{},
repoError: fmt.Errorf("error"),
expected: []map[string]string{},
expectedError: fmt.Errorf("error"),
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := argoCDServiceMock{mock: &mock.Mock{}}
argoCDServiceMock.mock.On("GetDirectories", mock.Anything, mock.Anything, mock.Anything).Return(testCaseCopy.repoApps, testCaseCopy.repoError)
var gitGenerator = NewGitGenerator(argoCDServiceMock)
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
Git: &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: testCaseCopy.directories,
},
}},
},
}
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], nil)
if testCaseCopy.expectedError != nil {
assert.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, testCaseCopy.expected, got)
}
argoCDServiceMock.mock.AssertExpectations(t)
})
}
}
func TestGitGenerateParamsFromFiles(t *testing.T) {
cases := []struct {
name string
// files is the list of paths/globs to match
files []argoprojiov1alpha1.GitFileGeneratorItem
// repoFileContents maps repo path to the literal contents of that path
repoFileContents map[string][]byte
// if repoPathsError is non-nil, the call to GetPaths(...) will return this error value
repoPathsError error
expected []map[string]string
expectedError error
}{
{
name: "happy flow: create params from git files",
files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`{
"cluster": {
"owner": "john.doe@example.com",
"name": "production",
"address": "https://kubernetes.default.svc"
},
"key1": "val1",
"key2": {
"key2_1": "val2_1",
"key2_2": {
"key2_2_1": "val2_2_1"
}
},
"key3": 123
}`),
"cluster-config/staging/config.json": []byte(`{
"cluster": {
"owner": "foo.bar@example.com",
"name": "staging",
"address": "https://kubernetes.default.svc"
}
}`),
},
repoPathsError: nil,
expected: []map[string]string{
{
"cluster.owner": "john.doe@example.com",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path.basenameNormalized": "production",
},
{
"cluster.owner": "foo.bar@example.com",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/staging",
"path.basename": "staging",
"path[0]": "cluster-config",
"path.basenameNormalized": "staging",
},
},
expectedError: nil,
},
{
name: "handles error during getting repo paths",
files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{},
repoPathsError: fmt.Errorf("paths error"),
expected: []map[string]string{},
expectedError: fmt.Errorf("paths error"),
},
{
name: "test invalid JSON file returns error",
files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`invalid json file`),
},
repoPathsError: nil,
expected: []map[string]string{},
expectedError: fmt.Errorf("unable to process file 'cluster-config/production/config.json': unable to parse file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type map[string]interface {}"),
},
{
name: "test JSON array",
files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`
[
{
"cluster": {
"owner": "john.doe@example.com",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": {
"one" : "two"
}
}
},
{
"cluster": {
"owner": "john.doe@example.com",
"name": "staging",
"address": "https://kubernetes.default.svc"
}
}
]`),
},
repoPathsError: nil,
expected: []map[string]string{
{
"cluster.owner": "john.doe@example.com",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"cluster.inner.one": "two",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path.basenameNormalized": "production",
},
{
"cluster.owner": "john.doe@example.com",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path.basenameNormalized": "production",
},
},
expectedError: nil,
},
{
name: "Test YAML flow",
files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.yaml"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.yaml": []byte(`
cluster:
owner: john.doe@example.com
name: production
address: https://kubernetes.default.svc
key1: val1
key2:
key2_1: val2_1
key2_2:
key2_2_1: val2_2_1
`),
"cluster-config/staging/config.yaml": []byte(`
cluster:
owner: foo.bar@example.com
name: staging
address: https://kubernetes.default.svc
`),
},
repoPathsError: nil,
expected: []map[string]string{
{
"cluster.owner": "john.doe@example.com",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path.basenameNormalized": "production",
},
{
"cluster.owner": "foo.bar@example.com",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/staging",
"path.basename": "staging",
"path[0]": "cluster-config",
"path.basenameNormalized": "staging",
},
},
expectedError: nil,
},
{
name: "test YAML array",
files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.yaml"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.yaml": []byte(`
- cluster:
owner: john.doe@example.com
name: production
address: https://kubernetes.default.svc
inner:
one: two
- cluster:
owner: john.doe@example.com
name: staging
address: https://kubernetes.default.svc`),
},
repoPathsError: nil,
expected: []map[string]string{
{
"cluster.owner": "john.doe@example.com",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"cluster.inner.one": "two",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path.basenameNormalized": "production",
},
{
"cluster.owner": "john.doe@example.com",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path.basenameNormalized": "production",
},
},
expectedError: nil,
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := argoCDServiceMock{mock: &mock.Mock{}}
argoCDServiceMock.mock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(testCaseCopy.repoFileContents, testCaseCopy.repoPathsError)
var gitGenerator = NewGitGenerator(argoCDServiceMock)
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
Git: &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: testCaseCopy.files,
},
}},
},
}
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], nil)
fmt.Println(got, err)
if testCaseCopy.expectedError != nil {
assert.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
assert.NoError(t, err)
assert.ElementsMatch(t, testCaseCopy.expected, got)
}
argoCDServiceMock.mock.AssertExpectations(t)
})
}
}

View file

@ -0,0 +1,32 @@
package generators
import (
"fmt"
"time"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
// Generator defines the interface implemented by all ApplicationSet generators.
type Generator interface {
// GenerateParams interprets the ApplicationSet and generates all relevant parameters for the application template.
// The expected / desired list of parameters is returned, it then will be render and reconciled
// against the current state of the Applications in the cluster.
GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error)
// GetRequeueAfter is the the generator can controller the next reconciled loop
// In case there is more then one generator the time will be the minimum of the times.
// In case NoRequeueAfter is empty, it will be ignored
GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration
// GetTemplate returns the inline template from the spec if there is any, or an empty object otherwise
GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate
}
var EmptyAppSetGeneratorError = fmt.Errorf("ApplicationSet is empty")
var NoRequeueAfter time.Duration
// DefaultRequeueAfterSeconds is used when GetRequeueAfter is not specified, it is the default time to wait before the next reconcile loop
const (
DefaultRequeueAfterSeconds = 3 * time.Minute
)

View file

@ -0,0 +1,74 @@
package generators
import (
"encoding/json"
"fmt"
"time"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
var _ Generator = (*ListGenerator)(nil)
type ListGenerator struct {
}
func NewListGenerator() Generator {
g := &ListGenerator{}
return g
}
func (g *ListGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
return NoRequeueAfter
}
func (g *ListGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.List.Template
}
func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, _ *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator == nil {
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.List == nil {
return nil, EmptyAppSetGeneratorError
}
res := make([]map[string]string, len(appSetGenerator.List.Elements))
for i, tmpItem := range appSetGenerator.List.Elements {
params := map[string]string{}
var element map[string]interface{}
err := json.Unmarshal(tmpItem.Raw, &element)
if err != nil {
return nil, fmt.Errorf("error unmarshling list element %v", err)
}
for key, value := range element {
if key == "values" {
values, ok := (value).(map[string]interface{})
if !ok {
return nil, fmt.Errorf("error parsing values map")
}
for k, v := range values {
value, ok := v.(string)
if !ok {
return nil, fmt.Errorf("error parsing value as string %v", err)
}
params[fmt.Sprintf("values.%s", k)] = value
}
} else {
v, ok := value.(string)
if !ok {
return nil, fmt.Errorf("error parsing value as string %v", err)
}
params[key] = v
}
}
res[i] = params
}
return res, nil
}

View file

@ -0,0 +1,39 @@
package generators
import (
"testing"
"github.com/stretchr/testify/assert"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestGenerateListParams(t *testing.T) {
testCases := []struct {
elements []apiextensionsv1.JSON
expected []map[string]string
}{
{
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
expected: []map[string]string{{"cluster": "cluster", "url": "url"}},
}, {
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}},
expected: []map[string]string{{"cluster": "cluster", "url": "url", "values.foo": "bar"}},
},
}
for _, testCase := range testCases {
var listGenerator = NewListGenerator()
got, err := listGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: testCase.elements,
}}, nil)
assert.NoError(t, err)
assert.ElementsMatch(t, testCase.expected, got)
}
}

View file

@ -0,0 +1,157 @@
package generators
import (
"fmt"
"time"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
var _ Generator = (*MatrixGenerator)(nil)
var (
ErrMoreThanTwoGenerators = fmt.Errorf("found more than two generators, Matrix support only two")
ErrLessThanTwoGenerators = fmt.Errorf("found less than two generators, Matrix support only two")
ErrMoreThenOneInnerGenerators = fmt.Errorf("found more than one generator in matrix.Generators")
)
type MatrixGenerator struct {
// The inner generators supported by the matrix generator (cluster, git, list...)
supportedGenerators map[string]Generator
}
func NewMatrixGenerator(supportedGenerators map[string]Generator) Generator {
m := &MatrixGenerator{
supportedGenerators: supportedGenerators,
}
return m
}
func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator.Matrix == nil {
return nil, EmptyAppSetGeneratorError
}
if len(appSetGenerator.Matrix.Generators) < 2 {
return nil, ErrLessThanTwoGenerators
}
if len(appSetGenerator.Matrix.Generators) > 2 {
return nil, ErrMoreThanTwoGenerators
}
res := []map[string]string{}
g0, err := m.getParams(appSetGenerator.Matrix.Generators[0], appSet)
if err != nil {
return nil, err
}
g1, err := m.getParams(appSetGenerator.Matrix.Generators[1], appSet)
if err != nil {
return nil, err
}
for _, a := range g0 {
for _, b := range g1 {
val, err := utils.CombineStringMaps(a, b)
if err != nil {
return nil, err
}
res = append(res, val)
}
}
return res, nil
}
func (m *MatrixGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
var matrix *argoprojiov1alpha1.MatrixGenerator
if appSetBaseGenerator.Matrix != nil {
// Since nested matrix generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here.
nestedMatrix, err := argoprojiov1alpha1.ToNestedMatrixGenerator(appSetBaseGenerator.Matrix)
if err != nil {
return nil, fmt.Errorf("unable to unmarshall nested matrix generator: %v", err)
}
if nestedMatrix != nil {
matrix = nestedMatrix.ToMatrixGenerator()
}
}
var mergeGenerator *argoprojiov1alpha1.MergeGenerator
if appSetBaseGenerator.Merge != nil {
// Since nested merge generator is represented as a JSON object in the CRD, we unmarshall it back to a Go struct here.
nestedMerge, err := argoprojiov1alpha1.ToNestedMergeGenerator(appSetBaseGenerator.Merge)
if err != nil {
return nil, fmt.Errorf("unable to unmarshall nested merge generator: %v", err)
}
if nestedMerge != nil {
mergeGenerator = nestedMerge.ToMergeGenerator()
}
}
t, err := Transform(
argoprojiov1alpha1.ApplicationSetGenerator{
List: appSetBaseGenerator.List,
Clusters: appSetBaseGenerator.Clusters,
Git: appSetBaseGenerator.Git,
SCMProvider: appSetBaseGenerator.SCMProvider,
ClusterDecisionResource: appSetBaseGenerator.ClusterDecisionResource,
PullRequest: appSetBaseGenerator.PullRequest,
Matrix: matrix,
Merge: mergeGenerator,
},
m.supportedGenerators,
argoprojiov1alpha1.ApplicationSetTemplate{},
appSet)
if err != nil {
return nil, fmt.Errorf("child generator returned an error on parameter generation: %v", err)
}
if len(t) == 0 {
return nil, fmt.Errorf("child generator generated no parameters")
}
if len(t) > 1 {
return nil, ErrMoreThenOneInnerGenerators
}
return t[0].Params, nil
}
const maxDuration time.Duration = 1<<63 - 1
func (m *MatrixGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
res := maxDuration
var found bool
for _, r := range appSetGenerator.Matrix.Generators {
base := &argoprojiov1alpha1.ApplicationSetGenerator{
List: r.List,
Clusters: r.Clusters,
Git: r.Git,
}
generators := GetRelevantGenerators(base, m.supportedGenerators)
for _, g := range generators {
temp := g.GetRequeueAfter(base)
if temp < res && temp != NoRequeueAfter {
found = true
res = temp
}
}
}
if found {
return res
} else {
return NoRequeueAfter
}
}
func (m *MatrixGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.Matrix.Template
}

View file

@ -0,0 +1,284 @@
package generators
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestMatrixGenerate(t *testing.T) {
gitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
}
listGenerator := &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}},
}
testCases := []struct {
name string
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
expectedErr error
expected []map[string]string
}{
{
name: "happy flow - generate params",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
{
List: listGenerator,
},
},
expected: []map[string]string{
{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "cluster": "Cluster", "url": "Url"},
{"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2", "cluster": "Cluster", "url": "Url"},
},
},
{
name: "happy flow - generate params from two lists",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"a": "1"}`)},
{Raw: []byte(`{"a": "2"}`)},
},
},
},
{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"b": "1"}`)},
{Raw: []byte(`{"b": "2"}`)},
},
},
},
},
expected: []map[string]string{
{"a": "1", "b": "1"},
{"a": "1", "b": "2"},
{"a": "2", "b": "1"},
{"a": "2", "b": "2"},
},
},
{
name: "returns error if there is less than two base generators",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
},
expectedErr: ErrLessThanTwoGenerators,
},
{
name: "returns error if there is more than two base generators",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: listGenerator,
},
{
List: listGenerator,
},
{
List: listGenerator,
},
},
expectedErr: ErrMoreThanTwoGenerators,
},
{
name: "returns error if there is more than one inner generator in the first base generator",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
List: listGenerator,
},
{
Git: gitGenerator,
},
},
expectedErr: ErrMoreThenOneInnerGenerators,
},
{
name: "returns error if there is more than one inner generator in the second base generator",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: listGenerator,
},
{
Git: gitGenerator,
List: listGenerator,
},
},
expectedErr: ErrMoreThenOneInnerGenerators,
},
}
for _, testCase := range testCases {
testCaseCopy := testCase // Since tests may run in parallel
t.Run(testCaseCopy.name, func(t *testing.T) {
mock := &generatorMock{}
appSet := &argoprojiov1alpha1.ApplicationSet{}
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
List: g.List,
}
mock.On("GenerateParams", &gitGeneratorSpec, appSet).Return([]map[string]string{
{
"path": "app1",
"path.basename": "app1",
"path.basenameNormalized": "app1",
},
{
"path": "app2",
"path.basename": "app2",
"path.basenameNormalized": "app2",
},
}, nil)
mock.On("GetTemplate", &gitGeneratorSpec).
Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
}
var matrixGenerator = NewMatrixGenerator(
map[string]Generator{
"Git": mock,
"List": &ListGenerator{},
},
)
got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
}, appSet)
if testCaseCopy.expectedErr != nil {
assert.EqualError(t, err, testCaseCopy.expectedErr.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, testCaseCopy.expected, got)
}
})
}
}
func TestMatrixGetRequeueAfter(t *testing.T) {
gitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
}
listGenerator := &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}},
}
testCases := []struct {
name string
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
gitGetRequeueAfter time.Duration
expected time.Duration
}{
{
name: "return NoRequeueAfter if all the inner baseGenerators returns it",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
{
List: listGenerator,
},
},
gitGetRequeueAfter: NoRequeueAfter,
expected: NoRequeueAfter,
},
{
name: "returns the minimal time",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
{
List: listGenerator,
},
},
gitGetRequeueAfter: time.Duration(1),
expected: time.Duration(1),
},
}
for _, testCase := range testCases {
testCaseCopy := testCase // Since tests may run in parallel
t.Run(testCaseCopy.name, func(t *testing.T) {
mock := &generatorMock{}
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
List: g.List,
}
mock.On("GetRequeueAfter", &gitGeneratorSpec).Return(testCaseCopy.gitGetRequeueAfter, nil)
}
var matrixGenerator = NewMatrixGenerator(
map[string]Generator{
"Git": mock,
"List": &ListGenerator{},
},
)
got := matrixGenerator.GetRequeueAfter(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
})
assert.Equal(t, testCaseCopy.expected, got)
})
}
}
type generatorMock struct {
mock.Mock
}
func (g *generatorMock) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
args := g.Called(appSetGenerator)
return args.Get(0).(*argoprojiov1alpha1.ApplicationSetTemplate)
}
func (g *generatorMock) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
args := g.Called(appSetGenerator, appSet)
return args.Get(0).([]map[string]string), args.Error(1)
}
func (g *generatorMock) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
args := g.Called(appSetGenerator)
return args.Get(0).(time.Duration)
}

View file

@ -0,0 +1,215 @@
package generators
import (
"encoding/json"
"fmt"
"time"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
var _ Generator = (*MergeGenerator)(nil)
var (
ErrLessThanTwoGeneratorsInMerge = fmt.Errorf("found less than two generators, Merge requires two or more")
ErrNoMergeKeys = fmt.Errorf("no merge keys were specified, Merge requires at least one")
ErrNonUniqueParamSets = fmt.Errorf("the parameters from a generator were not unique by the given mergeKeys, Merge requires all param sets to be unique")
)
type MergeGenerator struct {
// The inner generators supported by the merge generator (cluster, git, list...)
supportedGenerators map[string]Generator
}
// NewMergeGenerator returns a MergeGenerator which allows the given supportedGenerators as child generators.
func NewMergeGenerator(supportedGenerators map[string]Generator) Generator {
m := &MergeGenerator{
supportedGenerators: supportedGenerators,
}
return m
}
// getParamSetsForAllGenerators generates params for each child generator in a MergeGenerator. Param sets are returned
// in slices ordered according to the order of the given generators.
func (m *MergeGenerator) getParamSetsForAllGenerators(generators []argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([][]map[string]string, error) {
var paramSets [][]map[string]string
for _, generator := range generators {
generatorParamSets, err := m.getParams(generator, appSet)
if err != nil {
return nil, err
}
// concatenate param lists produced by each generator
paramSets = append(paramSets, generatorParamSets)
}
return paramSets, nil
}
// GenerateParams gets the params produced by the MergeGenerator.
func (m *MergeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator.Merge == nil {
return nil, EmptyAppSetGeneratorError
}
if len(appSetGenerator.Merge.Generators) < 2 {
return nil, ErrLessThanTwoGeneratorsInMerge
}
paramSetsFromGenerators, err := m.getParamSetsForAllGenerators(appSetGenerator.Merge.Generators, appSet)
if err != nil {
return nil, err
}
baseParamSetsByMergeKey, err := getParamSetsByMergeKey(appSetGenerator.Merge.MergeKeys, paramSetsFromGenerators[0])
if err != nil {
return nil, err
}
for _, paramSets := range paramSetsFromGenerators[1:] {
paramSetsByMergeKey, err := getParamSetsByMergeKey(appSetGenerator.Merge.MergeKeys, paramSets)
if err != nil {
return nil, err
}
for mergeKeyValue, baseParamSet := range baseParamSetsByMergeKey {
if overrideParamSet, exists := paramSetsByMergeKey[mergeKeyValue]; exists {
overriddenParamSet, err := utils.CombineStringMapsAllowDuplicates(baseParamSet, overrideParamSet)
if err != nil {
return nil, err
}
baseParamSetsByMergeKey[mergeKeyValue] = overriddenParamSet
}
}
}
mergedParamSets := make([]map[string]string, len(baseParamSetsByMergeKey))
var i = 0
for _, mergedParamSet := range baseParamSetsByMergeKey {
mergedParamSets[i] = mergedParamSet
i += 1
}
return mergedParamSets, nil
}
// getParamSetsByMergeKey converts the given list of parameter sets to a map of parameter sets where the key is the
// unique key of the parameter set as determined by the given mergeKeys. If any two parameter sets share the same merge
// key, getParamSetsByMergeKey will throw NonUniqueParamSets.
func getParamSetsByMergeKey(mergeKeys []string, paramSets []map[string]string) (map[string]map[string]string, error) {
if len(mergeKeys) < 1 {
return nil, ErrNoMergeKeys
}
deDuplicatedMergeKeys := make(map[string]bool, len(mergeKeys))
for _, mergeKey := range mergeKeys {
deDuplicatedMergeKeys[mergeKey] = false
}
paramSetsByMergeKey := make(map[string]map[string]string, len(paramSets))
for _, paramSet := range paramSets {
paramSetKey := make(map[string]string)
for mergeKey := range deDuplicatedMergeKeys {
paramSetKey[mergeKey] = paramSet[mergeKey]
}
paramSetKeyJson, err := json.Marshal(paramSetKey)
if err != nil {
return nil, err
}
paramSetKeyString := string(paramSetKeyJson)
if _, exists := paramSetsByMergeKey[paramSetKeyString]; exists {
return nil, fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, paramSetKeyString)
}
paramSetsByMergeKey[paramSetKeyString] = paramSet
}
return paramSetsByMergeKey, nil
}
// getParams get the parameters generated by this generator.
func (m *MergeGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
var matrix *argoprojiov1alpha1.MatrixGenerator
if appSetBaseGenerator.Matrix != nil {
nestedMatrix, err := argoprojiov1alpha1.ToNestedMatrixGenerator(appSetBaseGenerator.Matrix)
if err != nil {
return nil, err
}
if nestedMatrix != nil {
matrix = nestedMatrix.ToMatrixGenerator()
}
}
var mergeGenerator *argoprojiov1alpha1.MergeGenerator
if appSetBaseGenerator.Merge != nil {
nestedMerge, err := argoprojiov1alpha1.ToNestedMergeGenerator(appSetBaseGenerator.Merge)
if err != nil {
return nil, err
}
if nestedMerge != nil {
mergeGenerator = nestedMerge.ToMergeGenerator()
}
}
t, err := Transform(
argoprojiov1alpha1.ApplicationSetGenerator{
List: appSetBaseGenerator.List,
Clusters: appSetBaseGenerator.Clusters,
Git: appSetBaseGenerator.Git,
SCMProvider: appSetBaseGenerator.SCMProvider,
ClusterDecisionResource: appSetBaseGenerator.ClusterDecisionResource,
PullRequest: appSetBaseGenerator.PullRequest,
Matrix: matrix,
Merge: mergeGenerator,
},
m.supportedGenerators,
argoprojiov1alpha1.ApplicationSetTemplate{},
appSet)
if err != nil {
return nil, fmt.Errorf("child generator returned an error on parameter generation: %v", err)
}
if len(t) == 0 {
return nil, fmt.Errorf("child generator generated no parameters")
}
if len(t) > 1 {
return nil, ErrMoreThenOneInnerGenerators
}
return t[0].Params, nil
}
func (m *MergeGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
res := maxDuration
var found bool
for _, r := range appSetGenerator.Merge.Generators {
base := &argoprojiov1alpha1.ApplicationSetGenerator{
List: r.List,
Clusters: r.Clusters,
Git: r.Git,
}
generators := GetRelevantGenerators(base, m.supportedGenerators)
for _, g := range generators {
temp := g.GetRequeueAfter(base)
if temp < res && temp != NoRequeueAfter {
found = true
res = temp
}
}
}
if found {
return res
} else {
return NoRequeueAfter
}
}
// GetTemplate gets the Template field for the MergeGenerator.
func (m *MergeGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.Merge.Template
}

View file

@ -0,0 +1,328 @@
package generators
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func getNestedListGenerator(json string) *argoprojiov1alpha1.ApplicationSetNestedGenerator {
return &argoprojiov1alpha1.ApplicationSetNestedGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(json)}},
},
}
}
func getTerminalListGeneratorMultiple(jsons []string) argoprojiov1alpha1.ApplicationSetTerminalGenerator {
elements := make([]apiextensionsv1.JSON, len(jsons))
for i, json := range jsons {
elements[i] = apiextensionsv1.JSON{Raw: []byte(json)}
}
generator := argoprojiov1alpha1.ApplicationSetTerminalGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: elements,
},
}
return generator
}
func listOfMapsToSet(maps []map[string]string) (map[string]bool, error) {
set := make(map[string]bool, len(maps))
for _, paramMap := range maps {
paramMapAsJson, err := json.Marshal(paramMap)
if err != nil {
return nil, err
}
set[string(paramMapAsJson)] = false
}
return set, nil
}
func TestMergeGenerate(t *testing.T) {
testCases := []struct {
name string
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
mergeKeys []string
expectedErr error
expected []map[string]string
}{
{
name: "no generators",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{},
mergeKeys: []string{"b"},
expectedErr: ErrLessThanTwoGeneratorsInMerge,
},
{
name: "one generator",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
*getNestedListGenerator(`{"a": "1_1","b": "same","c": "1_3"}`),
},
mergeKeys: []string{"b"},
expectedErr: ErrLessThanTwoGeneratorsInMerge,
},
{
name: "happy flow - generate paramSets",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
*getNestedListGenerator(`{"a": "1_1","b": "same","c": "1_3"}`),
*getNestedListGenerator(`{"a": "2_1","b": "same"}`),
*getNestedListGenerator(`{"a": "3_1","b": "different","c": "3_3"}`), // gets ignored because its merge key value isn't in the base params set
},
mergeKeys: []string{"b"},
expected: []map[string]string{
{"a": "2_1", "b": "same", "c": "1_3"},
},
},
{
name: "merge keys absent - do not merge",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
*getNestedListGenerator(`{"a": "a"}`),
*getNestedListGenerator(`{"a": "a"}`),
},
mergeKeys: []string{"b"},
expected: []map[string]string{
{"a": "a"},
},
},
{
name: "merge key present in first set, absent in second - do not merge",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
*getNestedListGenerator(`{"a": "a"}`),
*getNestedListGenerator(`{"b": "b"}`),
},
mergeKeys: []string{"b"},
expected: []map[string]string{
{"a": "a"},
},
},
{
name: "merge nested matrix with some lists",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Matrix: toAPIExtensionsJSON(t, &argoprojiov1alpha1.NestedMatrixGenerator{
Generators: []argoprojiov1alpha1.ApplicationSetTerminalGenerator{
getTerminalListGeneratorMultiple([]string{`{"a": "1"}`, `{"a": "2"}`}),
getTerminalListGeneratorMultiple([]string{`{"b": "1"}`, `{"b": "2"}`}),
},
}),
},
*getNestedListGenerator(`{"a": "1", "b": "1", "c": "added"}`),
},
mergeKeys: []string{"a", "b"},
expected: []map[string]string{
{"a": "1", "b": "1", "c": "added"},
{"a": "1", "b": "2"},
{"a": "2", "b": "1"},
{"a": "2", "b": "2"},
},
},
{
name: "merge nested merge with some lists",
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Merge: toAPIExtensionsJSON(t, &argoprojiov1alpha1.NestedMergeGenerator{
MergeKeys: []string{"a"},
Generators: []argoprojiov1alpha1.ApplicationSetTerminalGenerator{
getTerminalListGeneratorMultiple([]string{`{"a": "1", "b": "1"}`, `{"a": "2", "b": "2"}`}),
getTerminalListGeneratorMultiple([]string{`{"a": "1", "b": "3", "c": "added"}`, `{"a": "3", "b": "2"}`}), // First gets merged, second gets ignored
},
}),
},
*getNestedListGenerator(`{"a": "1", "b": "3", "d": "added"}`),
},
mergeKeys: []string{"a", "b"},
expected: []map[string]string{
{"a": "1", "b": "3", "c": "added", "d": "added"},
{"a": "2", "b": "2"},
},
},
}
for _, testCase := range testCases {
testCaseCopy := testCase // since tests may run in parallel
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
appSet := &argoprojiov1alpha1.ApplicationSet{}
var mergeGenerator = NewMergeGenerator(
map[string]Generator{
"List": &ListGenerator{},
"Matrix": &MatrixGenerator{
supportedGenerators: map[string]Generator{
"List": &ListGenerator{},
},
},
"Merge": &MergeGenerator{
supportedGenerators: map[string]Generator{
"List": &ListGenerator{},
},
},
},
)
got, err := mergeGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Merge: &argoprojiov1alpha1.MergeGenerator{
Generators: testCaseCopy.baseGenerators,
MergeKeys: testCaseCopy.mergeKeys,
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
}, appSet)
if testCaseCopy.expectedErr != nil {
assert.EqualError(t, err, testCaseCopy.expectedErr.Error())
} else {
expectedSet, err := listOfMapsToSet(testCaseCopy.expected)
assert.NoError(t, err)
actualSet, err := listOfMapsToSet(got)
assert.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, expectedSet, actualSet)
}
})
}
}
func toAPIExtensionsJSON(t *testing.T, g interface{}) *apiextensionsv1.JSON {
resVal, err := json.Marshal(g)
if err != nil {
t.Error("unable to unmarshal json", g)
return nil
}
res := &apiextensionsv1.JSON{Raw: resVal}
return res
}
func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
testCases := []struct {
name string
mergeKeys []string
paramSets []map[string]string
expectedErr error
expected map[string]map[string]string
}{
{
name: "no merge keys",
mergeKeys: []string{},
expectedErr: ErrNoMergeKeys,
},
{
name: "no paramSets",
mergeKeys: []string{"key"},
expected: make(map[string]map[string]string),
},
{
name: "simple key, unique paramSets",
mergeKeys: []string{"key"},
paramSets: []map[string]string{{"key": "a"}, {"key": "b"}},
expected: map[string]map[string]string{
`{"key":"a"}`: {"key": "a"},
`{"key":"b"}`: {"key": "b"},
},
},
{
name: "simple key, non-unique paramSets",
mergeKeys: []string{"key"},
paramSets: []map[string]string{{"key": "a"}, {"key": "b"}, {"key": "b"}},
expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key":"b"}`),
},
{
name: "simple key, duplicated key name, unique paramSets",
mergeKeys: []string{"key", "key"},
paramSets: []map[string]string{{"key": "a"}, {"key": "b"}},
expected: map[string]map[string]string{
`{"key":"a"}`: {"key": "a"},
`{"key":"b"}`: {"key": "b"},
},
},
{
name: "simple key, duplicated key name, non-unique paramSets",
mergeKeys: []string{"key", "key"},
paramSets: []map[string]string{{"key": "a"}, {"key": "b"}, {"key": "b"}},
expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key":"b"}`),
},
{
name: "compound key, unique paramSets",
mergeKeys: []string{"key1", "key2"},
paramSets: []map[string]string{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "b"},
{"key1": "b", "key2": "a"},
},
expected: map[string]map[string]string{
`{"key1":"a","key2":"a"}`: {"key1": "a", "key2": "a"},
`{"key1":"a","key2":"b"}`: {"key1": "a", "key2": "b"},
`{"key1":"b","key2":"a"}`: {"key1": "b", "key2": "a"},
},
},
{
name: "compound key, duplicate key names, unique paramSets",
mergeKeys: []string{"key1", "key1", "key2"},
paramSets: []map[string]string{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "b"},
{"key1": "b", "key2": "a"},
},
expected: map[string]map[string]string{
`{"key1":"a","key2":"a"}`: {"key1": "a", "key2": "a"},
`{"key1":"a","key2":"b"}`: {"key1": "a", "key2": "b"},
`{"key1":"b","key2":"a"}`: {"key1": "b", "key2": "a"},
},
},
{
name: "compound key, non-unique paramSets",
mergeKeys: []string{"key1", "key2"},
paramSets: []map[string]string{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "a"},
{"key1": "b", "key2": "a"},
},
expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key1":"a","key2":"a"}`),
},
{
name: "compound key, duplicate key names, non-unique paramSets",
mergeKeys: []string{"key1", "key1", "key2"},
paramSets: []map[string]string{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "a"},
{"key1": "b", "key2": "a"},
},
expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key1":"a","key2":"a"}`),
},
}
for _, testCase := range testCases {
testCaseCopy := testCase // since tests may run in parallel
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
got, err := getParamSetsByMergeKey(testCaseCopy.mergeKeys, testCaseCopy.paramSets)
if testCaseCopy.expectedErr != nil {
assert.EqualError(t, err, testCaseCopy.expectedErr.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, testCaseCopy.expected, got)
}
})
}
}

View file

@ -0,0 +1,114 @@
package generators
import (
"context"
"fmt"
"strconv"
"time"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
pullrequest "github.com/argoproj/argo-cd/v2/applicationset/services/pull_request"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
var _ Generator = (*PullRequestGenerator)(nil)
const (
DefaultPullRequestRequeueAfterSeconds = 30 * time.Minute
)
type PullRequestGenerator struct {
client client.Client
selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
}
func NewPullRequestGenerator(client client.Client) Generator {
g := &PullRequestGenerator{
client: client,
}
g.selectServiceProviderFunc = g.selectServiceProvider
return g
}
func (g *PullRequestGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
// Return a requeue default of 30 minutes, if no default is specified.
if appSetGenerator.PullRequest.RequeueAfterSeconds != nil {
return time.Duration(*appSetGenerator.PullRequest.RequeueAfterSeconds) * time.Second
}
return DefaultPullRequestRequeueAfterSeconds
}
func (g *PullRequestGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.PullRequest.Template
}
func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator == nil {
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.PullRequest == nil {
return nil, EmptyAppSetGeneratorError
}
ctx := context.Background()
svc, err := g.selectServiceProviderFunc(ctx, appSetGenerator.PullRequest, applicationSetInfo)
if err != nil {
return nil, fmt.Errorf("failed to select pull request service provider: %v", err)
}
pulls, err := svc.List(ctx)
if err != nil {
return nil, fmt.Errorf("error listing repos: %v", err)
}
params := make([]map[string]string, 0, len(pulls))
for _, pull := range pulls {
params = append(params, map[string]string{
"number": strconv.Itoa(pull.Number),
"branch": pull.Branch,
"head_sha": pull.HeadSHA,
})
}
return params, nil
}
// selectServiceProvider selects the provider to get pull requests from the configuration
func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, generatorConfig *argoprojiov1alpha1.PullRequestGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
if generatorConfig.Github != nil {
providerConfig := generatorConfig.Github
token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %v", err)
}
return pullrequest.NewGithubService(ctx, token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Labels)
}
return nil, fmt.Errorf("no Pull Request provider implementation configured")
}
// getSecretRef gets the value of the key for the specified Secret resource.
func (g *PullRequestGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
if ref == nil {
return "", nil
}
secret := &corev1.Secret{}
err := g.client.Get(
ctx,
client.ObjectKey{
Name: ref.SecretName,
Namespace: namespace,
},
secret)
if err != nil {
return "", fmt.Errorf("error fetching secret %s/%s: %v", namespace, ref.SecretName, err)
}
tokenBytes, ok := secret.Data[ref.Key]
if !ok {
return "", fmt.Errorf("key %q in secret %s/%s not found", ref.Key, namespace, ref.SecretName)
}
return string(tokenBytes), nil
}

View file

@ -0,0 +1,136 @@
package generators
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
pullrequest "github.com/argoproj/argo-cd/v2/applicationset/services/pull_request"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestPullRequestGithubGenerateParams(t *testing.T) {
ctx := context.Background()
cases := []struct {
selectFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
expected []map[string]string
expectedErr error
}{
{
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
return pullrequest.NewFakeService(
ctx,
[]*pullrequest.PullRequest{
&pullrequest.PullRequest{
Number: 1,
Branch: "branch1",
HeadSHA: "089d92cbf9ff857a39e6feccd32798ca700fb958",
},
},
nil,
)
},
expected: []map[string]string{
{
"number": "1",
"branch": "branch1",
"head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958",
},
},
expectedErr: nil,
},
{
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
return pullrequest.NewFakeService(
ctx,
nil,
fmt.Errorf("fake error"),
)
},
expected: nil,
expectedErr: fmt.Errorf("error listing repos: fake error"),
},
}
for _, c := range cases {
gen := PullRequestGenerator{
selectServiceProviderFunc: c.selectFunc,
}
generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{
PullRequest: &argoprojiov1alpha1.PullRequestGenerator{},
}
got, gotErr := gen.GenerateParams(&generatorConfig, nil)
assert.Equal(t, c.expectedErr, gotErr)
assert.ElementsMatch(t, c.expected, got)
}
}
func TestPullRequestGetSecretRef(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "test"},
Data: map[string][]byte{
"my-token": []byte("secret"),
},
}
gen := &PullRequestGenerator{client: fake.NewClientBuilder().WithObjects(secret).Build()}
ctx := context.Background()
cases := []struct {
name, namespace, token string
ref *argoprojiov1alpha1.SecretRef
hasError bool
}{
{
name: "valid ref",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "test",
token: "secret",
hasError: false,
},
{
name: "nil ref",
ref: nil,
namespace: "test",
token: "",
hasError: false,
},
{
name: "wrong name",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "other", Key: "my-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong key",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "other-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong namespace",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "other",
token: "",
hasError: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
token, err := gen.getSecretRef(ctx, c.ref, c.namespace)
if c.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
assert.Equal(t, c.token, token)
})
}
}

View file

@ -0,0 +1,124 @@
package generators
import (
"context"
"fmt"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
var _ Generator = (*SCMProviderGenerator)(nil)
const (
DefaultSCMProviderRequeueAfterSeconds = 30 * time.Minute
)
type SCMProviderGenerator struct {
client client.Client
// Testing hooks.
overrideProvider scm_provider.SCMProviderService
}
func NewSCMProviderGenerator(client client.Client) Generator {
return &SCMProviderGenerator{client: client}
}
func (g *SCMProviderGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
// Return a requeue default of 30 minutes, if no default is specified.
if appSetGenerator.SCMProvider.RequeueAfterSeconds != nil {
return time.Duration(*appSetGenerator.SCMProvider.RequeueAfterSeconds) * time.Second
}
return DefaultSCMProviderRequeueAfterSeconds
}
func (g *SCMProviderGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.SCMProvider.Template
}
func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]string, error) {
if appSetGenerator == nil {
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.SCMProvider == nil {
return nil, EmptyAppSetGeneratorError
}
ctx := context.Background()
// Create the SCM provider helper.
providerConfig := appSetGenerator.SCMProvider
var provider scm_provider.SCMProviderService
if g.overrideProvider != nil {
provider = g.overrideProvider
} else if providerConfig.Github != nil {
token, err := g.getSecretRef(ctx, providerConfig.Github.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Github token: %v", err)
}
provider, err = scm_provider.NewGithubProvider(ctx, providerConfig.Github.Organization, token, providerConfig.Github.API, providerConfig.Github.AllBranches)
if err != nil {
return nil, fmt.Errorf("error initializing Github service: %v", err)
}
} else if providerConfig.Gitlab != nil {
token, err := g.getSecretRef(ctx, providerConfig.Gitlab.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Gitlab token: %v", err)
}
provider, err = scm_provider.NewGitlabProvider(ctx, providerConfig.Gitlab.Group, token, providerConfig.Gitlab.API, providerConfig.Gitlab.AllBranches, providerConfig.Gitlab.IncludeSubgroups)
if err != nil {
return nil, fmt.Errorf("error initializing Gitlab service: %v", err)
}
} else {
return nil, fmt.Errorf("no SCM provider implementation configured")
}
// Find all the available repos.
repos, err := scm_provider.ListRepos(ctx, provider, providerConfig.Filters, providerConfig.CloneProtocol)
if err != nil {
return nil, fmt.Errorf("error listing repos: %v", err)
}
params := make([]map[string]string, 0, len(repos))
for _, repo := range repos {
params = append(params, map[string]string{
"organization": repo.Organization,
"repository": repo.Repository,
"url": repo.URL,
"branch": repo.Branch,
"sha": repo.SHA,
"labels": strings.Join(repo.Labels, ","),
})
}
return params, nil
}
func (g *SCMProviderGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
if ref == nil {
return "", nil
}
secret := &corev1.Secret{}
err := g.client.Get(
ctx,
client.ObjectKey{
Name: ref.SecretName,
Namespace: namespace,
},
secret)
if err != nil {
return "", fmt.Errorf("error fetching secret %s/%s: %v", namespace, ref.SecretName, err)
}
tokenBytes, ok := secret.Data[ref.Key]
if !ok {
return "", fmt.Errorf("key %q in secret %s/%s not found", ref.Key, namespace, ref.SecretName)
}
return string(tokenBytes), nil
}

View file

@ -0,0 +1,115 @@
package generators
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestSCMProviderGetSecretRef(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "test"},
Data: map[string][]byte{
"my-token": []byte("secret"),
},
}
gen := &SCMProviderGenerator{client: fake.NewClientBuilder().WithObjects(secret).Build()}
ctx := context.Background()
cases := []struct {
name, namespace, token string
ref *argoprojiov1alpha1.SecretRef
hasError bool
}{
{
name: "valid ref",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "test",
token: "secret",
hasError: false,
},
{
name: "nil ref",
ref: nil,
namespace: "test",
token: "",
hasError: false,
},
{
name: "wrong name",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "other", Key: "my-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong key",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "other-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong namespace",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "other",
token: "",
hasError: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
token, err := gen.getSecretRef(ctx, c.ref, c.namespace)
if c.hasError {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
assert.Equal(t, c.token, token)
})
}
}
func TestSCMProviderGenerateParams(t *testing.T) {
mockProvider := &scm_provider.MockProvider{
Repos: []*scm_provider.Repository{
{
Organization: "myorg",
Repository: "repo1",
URL: "git@github.com:myorg/repo1.git",
Branch: "main",
SHA: "abcd1234",
Labels: []string{"prod", "staging"},
},
{
Organization: "myorg",
Repository: "repo2",
URL: "git@github.com:myorg/repo2.git",
Branch: "main",
SHA: "00000000",
},
},
}
gen := &SCMProviderGenerator{overrideProvider: mockProvider}
params, err := gen.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
SCMProvider: &argoprojiov1alpha1.SCMProviderGenerator{},
}, nil)
assert.Nil(t, err)
assert.Len(t, params, 2)
assert.Equal(t, "myorg", params[0]["organization"])
assert.Equal(t, "repo1", params[0]["repository"])
assert.Equal(t, "git@github.com:myorg/repo1.git", params[0]["url"])
assert.Equal(t, "main", params[0]["branch"])
assert.Equal(t, "abcd1234", params[0]["sha"])
assert.Equal(t, "prod,staging", params[0]["labels"])
assert.Equal(t, "repo2", params[1]["repository"])
}

View file

@ -0,0 +1,23 @@
package pull_request
import (
"context"
)
type FakeService struct {
listPullReuests []*PullRequest
listError error
}
var _ PullRequestService = (*FakeService)(nil)
func NewFakeService(_ context.Context, listPullReuests []*PullRequest, listError error) (PullRequestService, error) {
return &FakeService{
listPullReuests: listPullReuests,
listError: listError,
}, nil
}
func (g *FakeService) List(ctx context.Context) ([]*PullRequest, error) {
return g.listPullReuests, g.listError
}

View file

@ -0,0 +1,99 @@
package pull_request
import (
"context"
"fmt"
"os"
"github.com/google/go-github/v35/github"
"golang.org/x/oauth2"
)
type GithubService struct {
client *github.Client
owner string
repo string
labels []string
}
var _ PullRequestService = (*GithubService)(nil)
func NewGithubService(ctx context.Context, token, url, owner, repo string, labels []string) (PullRequestService, error) {
var ts oauth2.TokenSource
// Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
if token == "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token != "" {
ts = oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
}
httpClient := oauth2.NewClient(ctx, ts)
var client *github.Client
if url == "" {
client = github.NewClient(httpClient)
} else {
var err error
client, err = github.NewEnterpriseClient(url, url, httpClient)
if err != nil {
return nil, err
}
}
return &GithubService{
client: client,
owner: owner,
repo: repo,
labels: labels,
}, nil
}
func (g *GithubService) List(ctx context.Context) ([]*PullRequest, error) {
opts := &github.PullRequestListOptions{
ListOptions: github.ListOptions{
PerPage: 100,
},
}
pullRequests := []*PullRequest{}
for {
pulls, resp, err := g.client.PullRequests.List(ctx, g.owner, g.repo, opts)
if err != nil {
return nil, fmt.Errorf("error listing pull requests for %s/%s: %v", g.owner, g.repo, err)
}
for _, pull := range pulls {
if !containLabels(g.labels, pull.Labels) {
continue
}
pullRequests = append(pullRequests, &PullRequest{
Number: *pull.Number,
Branch: *pull.Head.Ref,
HeadSHA: *pull.Head.SHA,
})
}
if resp.NextPage == 0 {
break
}
opts.Page = resp.NextPage
}
return pullRequests, nil
}
// containLabels returns true if gotLabels contains expectedLabels
func containLabels(expectedLabels []string, gotLabels []*github.Label) bool {
for _, expected := range expectedLabels {
found := false
for _, got := range gotLabels {
if got.Name == nil {
continue
}
if expected == *got.Name {
found = true
break
}
}
if !found {
return false
}
}
return true
}

View file

@ -0,0 +1,59 @@
package pull_request
import (
"testing"
"github.com/google/go-github/v35/github"
)
func toPtr(s string) *string {
return &s
}
func TestContainLabels(t *testing.T) {
cases := []struct {
Name string
Labels []string
PullLabels []*github.Label
Expect bool
}{
{
Name: "Match labels",
Labels: []string{"label1", "label2"},
PullLabels: []*github.Label{
&github.Label{Name: toPtr("label1")},
&github.Label{Name: toPtr("label2")},
&github.Label{Name: toPtr("label3")},
},
Expect: true,
},
{
Name: "Not match labels",
Labels: []string{"label1", "label4"},
PullLabels: []*github.Label{
&github.Label{Name: toPtr("label1")},
&github.Label{Name: toPtr("label2")},
&github.Label{Name: toPtr("label3")},
},
Expect: false,
},
{
Name: "No specify",
Labels: []string{},
PullLabels: []*github.Label{
&github.Label{Name: toPtr("label1")},
&github.Label{Name: toPtr("label2")},
&github.Label{Name: toPtr("label3")},
},
Expect: true,
},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
if got := containLabels(c.Labels, c.PullLabels); got != c.Expect {
t.Errorf("expect: %v, got: %v", c.Expect, got)
}
})
}
}

View file

@ -0,0 +1,17 @@
package pull_request
import "context"
type PullRequest struct {
// Number is a number that will be the ID of the pull request.
Number int
// Branch is the name of the branch from which the pull request originated.
Branch string
// HeadSHA is the SHA of the HEAD from which the pull request originated.
HeadSHA string
}
type PullRequestService interface {
// List gets a list of pull requests.
List(ctx context.Context) ([]*PullRequest, error)
}

View file

@ -0,0 +1,151 @@
package services
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/git"
)
// RepositoryDB Is a lean facade for ArgoDB,
// Using a lean interface makes it more easy to test the functionality the git generator uses
type RepositoryDB interface {
GetRepository(ctx context.Context, url string) (*v1alpha1.Repository, error)
}
type argoCDService struct {
repositoriesDB RepositoryDB
storecreds git.CredsStore
}
type Repos interface {
// GetFiles returns content of files (not directories) within the target repo
GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error)
// GetDirectories returns a list of directories (not files) within the target repo
GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error)
}
func NewArgoCDService(db db.ArgoDB, gitCredStore git.CredsStore, repoServerAddress string) Repos {
return &argoCDService{
repositoriesDB: db.(RepositoryDB),
storecreds: gitCredStore,
}
}
func (a *argoCDService) GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error) {
repo, err := a.repositoriesDB.GetRepository(ctx, repoURL)
if err != nil {
return nil, fmt.Errorf("Error in GetRepository: %w", err)
}
gitRepoClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(a.storecreds), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy)
if err != nil {
return nil, err
}
err = checkoutRepo(gitRepoClient, revision)
if err != nil {
return nil, err
}
paths, err := gitRepoClient.LsFiles(pattern)
if err != nil {
return nil, fmt.Errorf("Error during listing files of local repo: %w", err)
}
res := map[string][]byte{}
for _, filePath := range paths {
bytes, err := os.ReadFile(filepath.Join(gitRepoClient.Root(), filePath))
if err != nil {
return nil, err
}
res[filePath] = bytes
}
return res, nil
}
func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) {
repo, err := a.repositoriesDB.GetRepository(ctx, repoURL)
if err != nil {
return nil, fmt.Errorf("Error in GetRepository: %w", err)
}
gitRepoClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(a.storecreds), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy)
if err != nil {
return nil, err
}
err = checkoutRepo(gitRepoClient, revision)
if err != nil {
return nil, err
}
filteredPaths := []string{}
repoRoot := gitRepoClient.Root()
if err := filepath.Walk(repoRoot, func(path string, info os.FileInfo, fnErr error) error {
if fnErr != nil {
return fnErr
}
if !info.IsDir() { // Skip files: directories only
return nil
}
fname := info.Name()
if strings.HasPrefix(fname, ".") { // Skip all folders starts with "."
return filepath.SkipDir
}
relativePath, err := filepath.Rel(repoRoot, path)
if err != nil {
return err
}
if relativePath == "." { // Exclude '.' from results
return nil
}
filteredPaths = append(filteredPaths, relativePath)
return nil
}); err != nil {
return nil, err
}
return filteredPaths, nil
}
func checkoutRepo(gitRepoClient git.Client, revision string) error {
err := gitRepoClient.Init()
if err != nil {
return fmt.Errorf("Error during initializing repo: %w", err)
}
err = gitRepoClient.Fetch(revision)
if err != nil {
return fmt.Errorf("Error during fetching repo: %w", err)
}
commitSHA, err := gitRepoClient.LsRemote(revision)
if err != nil {
return fmt.Errorf("Error during fetching commitSHA: %w", err)
}
err = gitRepoClient.Checkout(commitSHA, true)
if err != nil {
return fmt.Errorf("Error during repo checkout: %w", err)
}
return nil
}

View file

@ -0,0 +1,233 @@
package services
import (
"context"
"fmt"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
type ArgocdRepositoryMock struct {
mock *mock.Mock
}
func (a ArgocdRepositoryMock) GetRepository(ctx context.Context, url string) (*v1alpha1.Repository, error) {
args := a.mock.Called(ctx, url)
return args.Get(0).(*v1alpha1.Repository), args.Error(1)
}
func TestGetDirectories(t *testing.T) {
// Hardcode a specific revision to changes to argocd-example-apps from regressing this test:
// Author: Alexander Matyushentsev <Alexander_Matyushentsev@intuit.com>
// Date: Sun Jan 31 09:54:53 2021 -0800
// chore: downgrade kustomize guestbook image tag (#73)
exampleRepoRevision := "08f72e2a309beab929d9fd14626071b1a61a47f9"
for _, c := range []struct {
name string
repoURL string
revision string
repoRes *v1alpha1.Repository
repoErr error
expected []string
expectedError error
}{
{
name: "All child folders should be returned",
repoURL: "https://github.com/argoproj/argocd-example-apps/",
revision: exampleRepoRevision,
repoRes: &v1alpha1.Repository{
Repo: "https://github.com/argoproj/argocd-example-apps/",
},
repoErr: nil,
expected: []string{"apps", "apps/templates", "blue-green", "blue-green/templates", "guestbook", "helm-dependency",
"helm-guestbook", "helm-guestbook/templates", "helm-hooks", "jsonnet-guestbook", "jsonnet-guestbook-tla",
"ksonnet-guestbook", "ksonnet-guestbook/components", "ksonnet-guestbook/environments", "ksonnet-guestbook/environments/default",
"ksonnet-guestbook/environments/dev", "ksonnet-guestbook/environments/prod", "kustomize-guestbook", "plugins", "plugins/kasane",
"plugins/kustomized-helm", "plugins/kustomized-helm/overlays", "pre-post-sync", "sock-shop", "sock-shop/base", "sync-waves"},
},
{
name: "If GetRepository returns an error, it should pass back to caller",
repoURL: "https://github.com/argoproj/argocd-example-apps/",
revision: exampleRepoRevision,
repoRes: &v1alpha1.Repository{
Repo: "https://github.com/argoproj/argocd-example-apps/",
},
repoErr: fmt.Errorf("Simulated error from GetRepository"),
expected: nil,
expectedError: fmt.Errorf("Error in GetRepository: Simulated error from GetRepository"),
},
{
name: "Test against repository containing no directories",
// Here I picked an arbitrary repository in argoproj-labs, with a commit containing no folders.
repoURL: "https://github.com/argoproj-labs/argo-workflows-operator/",
revision: "5f50933a576833b73b7a172909d8545a108685f4",
repoRes: &v1alpha1.Repository{
Repo: "https://github.com/argoproj-labs/argo-workflows-operator/",
},
repoErr: nil,
expected: []string{},
},
} {
cc := c
t.Run(cc.name, func(t *testing.T) {
argocdRepositoryMock := ArgocdRepositoryMock{mock: &mock.Mock{}}
argocdRepositoryMock.mock.On("GetRepository", mock.Anything, cc.repoURL).Return(cc.repoRes, cc.repoErr)
argocd := argoCDService{
repositoriesDB: argocdRepositoryMock,
}
got, err := argocd.GetDirectories(context.TODO(), cc.repoURL, cc.revision)
if cc.expectedError != nil {
assert.EqualError(t, err, cc.expectedError.Error())
} else {
sort.Strings(got)
sort.Strings(cc.expected)
assert.Equal(t, got, cc.expected)
assert.NoError(t, err)
}
})
}
}
func TestGetFiles(t *testing.T) {
// Hardcode a specific commit, so that changes to argoproj/argocd-example-apps/ don't break our tests
// "chore: downgrade kustomize guestbook image tag (#73)"
commitID := "08f72e2a309beab929d9fd14626071b1a61a47f9"
tests := []struct {
name string
repoURL string
revision string
pattern string
repoRes *v1alpha1.Repository
repoErr error
expectSubsetOfPaths []string
doesNotContainPaths []string
expectedError error
}{
{
name: "pull a specific revision of example apps and verify the list is expected",
repoRes: &v1alpha1.Repository{
Insecure: true,
InsecureIgnoreHostKey: true,
Repo: "https://github.com/argoproj/argocd-example-apps/",
},
repoURL: "https://github.com/argoproj/argocd-example-apps/",
revision: commitID,
pattern: "*",
expectSubsetOfPaths: []string{
"apps/Chart.yaml",
"apps/templates/helm-guestbook.yaml",
"apps/templates/helm-hooks.yaml",
"apps/templates/kustomize-guestbook.yaml",
"apps/templates/namespaces.yaml",
"apps/templates/sync-waves.yaml",
"apps/values.yaml",
"blue-green/.helmignore",
"blue-green/Chart.yaml",
"blue-green/README.md",
"blue-green/templates/NOTES.txt",
"blue-green/templates/rollout.yaml",
"blue-green/templates/services.yaml",
"blue-green/values.yaml",
"guestbook/guestbook-ui-deployment.yaml",
"guestbook/guestbook-ui-svc.yaml",
"kustomize-guestbook/guestbook-ui-deployment.yaml",
"kustomize-guestbook/guestbook-ui-svc.yaml",
"kustomize-guestbook/kustomization.yaml",
},
},
{
name: "pull an invalid revision, and confirm an error is returned",
repoRes: &v1alpha1.Repository{
Insecure: true,
InsecureIgnoreHostKey: true,
Repo: "https://github.com/argoproj/argocd-example-apps/",
},
repoURL: "https://github.com/argoproj/argocd-example-apps/",
revision: "this-tag-does-not-exist",
pattern: "*",
expectSubsetOfPaths: []string{},
expectedError: fmt.Errorf("Error during fetching repo: `git fetch origin this-tag-does-not-exist --tags --force` failed exit status 128: fatal: couldn't find remote ref this-tag-does-not-exist"),
},
{
name: "pull a specific revision of example apps, and use a ** pattern",
repoRes: &v1alpha1.Repository{
Insecure: true,
InsecureIgnoreHostKey: true,
Repo: "https://github.com/argoproj/argocd-example-apps/",
},
repoURL: "https://github.com/argoproj/argocd-example-apps/",
revision: commitID,
pattern: "**/*.yaml",
expectSubsetOfPaths: []string{
"apps/Chart.yaml",
"apps/templates/helm-guestbook.yaml",
"apps/templates/helm-hooks.yaml",
"apps/templates/kustomize-guestbook.yaml",
"apps/templates/namespaces.yaml",
"apps/templates/sync-waves.yaml",
"apps/values.yaml",
"blue-green/templates/rollout.yaml",
"blue-green/templates/services.yaml",
"blue-green/values.yaml",
"guestbook/guestbook-ui-deployment.yaml",
"guestbook/guestbook-ui-svc.yaml",
"kustomize-guestbook/guestbook-ui-deployment.yaml",
"kustomize-guestbook/guestbook-ui-svc.yaml",
"kustomize-guestbook/kustomization.yaml",
},
doesNotContainPaths: []string{
"blue-green/.helmignore",
"blue-green/README.md",
"blue-green/templates/NOTES.txt",
},
},
}
for _, cc := range tests {
// Get all the paths for a repository, and confirm that the expected subset of paths is found (or the expected error is returned)
t.Run(cc.name, func(t *testing.T) {
argocdRepositoryMock := ArgocdRepositoryMock{mock: &mock.Mock{}}
argocdRepositoryMock.mock.On("GetRepository", mock.Anything, cc.repoURL).Return(cc.repoRes, cc.repoErr)
argocd := argoCDService{
repositoriesDB: argocdRepositoryMock,
}
getPathsRes, err := argocd.GetFiles(context.Background(), cc.repoURL, cc.revision, cc.pattern)
if cc.expectedError == nil {
assert.NoError(t, err)
for _, path := range cc.expectSubsetOfPaths {
assert.Contains(t, getPathsRes, path, "Unable to locate path: %s", path)
}
for _, shouldNotContain := range cc.doesNotContainPaths {
assert.NotContains(t, getPathsRes, shouldNotContain, "GetPaths should not contain %s", shouldNotContain)
}
} else {
assert.EqualError(t, err, cc.expectedError.Error())
}
})
}
}

View file

@ -0,0 +1,177 @@
package scm_provider
import (
"context"
"fmt"
"net/http"
"strings"
bitbucket "github.com/ktrysmt/go-bitbucket"
)
type BitBucketCloudProvider struct {
client *ExtendedClient
allBranches bool
owner string
}
type ExtendedClient struct {
*bitbucket.Client
username string
password string
owner string
}
func (c *ExtendedClient) GetContents(repo *Repository, path string) (bool, error) {
urlStr := c.GetApiBaseURL()
// Getting file contents from V2 defined at https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/#api-repositories-workspace-repo-slug-src-commit-path-get
urlStr += fmt.Sprintf("/repositories/%s/%s/src/%s/%s?format=meta", c.owner, repo.Repository, repo.SHA, path)
body := strings.NewReader("")
req, err := http.NewRequest("GET", urlStr, body)
if err != nil {
return false, err
}
req.SetBasicAuth(c.username, c.password)
resp, err := c.HttpClient.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return false, nil
}
if resp.StatusCode == http.StatusOK {
return true, nil
}
return false, fmt.Errorf(resp.Status)
}
var _ SCMProviderService = &BitBucketCloudProvider{}
func NewBitBucketCloudProvider(ctx context.Context, owner string, user string, password string, allBranches bool) (*BitBucketCloudProvider, error) {
client := &ExtendedClient{
bitbucket.NewBasicAuth(user, password),
user,
password,
owner,
}
return &BitBucketCloudProvider{client: client, owner: owner, allBranches: allBranches}, nil
}
func (g *BitBucketCloudProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
repos := []*Repository{}
branches, err := g.listBranches(repo)
if err != nil {
return nil, fmt.Errorf("error listing branches for %s/%s: %v", repo.Organization, repo.Repository, err)
}
for _, branch := range branches {
hash, ok := branch.Target["hash"].(string)
if !ok {
return nil, fmt.Errorf("error getting SHA for branch for %s/%s/%s: %v", g.owner, repo.Repository, branch.Name, err)
}
repos = append(repos, &Repository{
Organization: repo.Organization,
Repository: repo.Repository,
URL: repo.URL,
Branch: branch.Name,
SHA: hash,
Labels: repo.Labels,
RepositoryId: repo.RepositoryId,
})
}
return repos, nil
}
func (g *BitBucketCloudProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
if cloneProtocol == "" {
cloneProtocol = "ssh"
}
opt := &bitbucket.RepositoriesOptions{
Owner: g.owner,
Role: "member",
}
repos := []*Repository{}
accountReposResp, err := g.client.Repositories.ListForAccount(opt)
if err != nil {
return nil, fmt.Errorf("error listing repositories for %s: %v", g.owner, err)
}
for _, bitBucketRepo := range accountReposResp.Items {
cloneUrl, err := findCloneURL(cloneProtocol, &bitBucketRepo)
if err != nil {
return nil, fmt.Errorf("error fetching clone url for repo %s: %v", bitBucketRepo.Slug, err)
}
repos = append(repos, &Repository{
Organization: g.owner,
Repository: bitBucketRepo.Slug,
Branch: bitBucketRepo.Mainbranch.Name,
URL: *cloneUrl,
Labels: []string{},
RepositoryId: bitBucketRepo.Uuid,
})
}
return repos, nil
}
func (g *BitBucketCloudProvider) RepoHasPath(ctx context.Context, repo *Repository, path string) (bool, error) {
contents, err := g.client.GetContents(repo, path)
if err != nil {
return false, err
}
if contents {
return true, nil
}
return false, nil
}
func (g *BitBucketCloudProvider) listBranches(repo *Repository) ([]bitbucket.RepositoryBranch, error) {
if !g.allBranches {
repoBranch, err := g.client.Repositories.Repository.GetBranch(&bitbucket.RepositoryBranchOptions{
Owner: g.owner,
RepoSlug: repo.Repository,
BranchName: repo.Branch,
})
if err != nil {
return nil, err
}
return []bitbucket.RepositoryBranch{
*repoBranch,
}, nil
}
branches, err := g.client.Repositories.Repository.ListBranches(&bitbucket.RepositoryBranchOptions{
Owner: g.owner,
RepoSlug: repo.Repository,
})
if err != nil {
return nil, err
}
return branches.Branches, nil
}
func findCloneURL(cloneProtocol string, repo *bitbucket.Repository) (*string, error) {
cloneLinks, ok := repo.Links["clone"].([]interface{})
if !ok {
return nil, fmt.Errorf("unknown type returned from repo links")
}
for _, link := range cloneLinks {
linkEntry, ok := link.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unknown type returned from clone link")
}
if linkEntry["name"] == cloneProtocol {
url, ok := linkEntry["href"].(string)
if !ok {
return nil, fmt.Errorf("could not find href for clone link")
}
return &url, nil
}
}
return nil, fmt.Errorf("unknown clone protocol for Bitbucket cloud %v", cloneProtocol)
}

View file

@ -0,0 +1,511 @@
package scm_provider
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestBitbucketHasRepo(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/repositories/test-owner/testmike/src/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/.gitignore2" {
res.WriteHeader(http.StatusNotFound)
_, err := res.Write([]byte(""))
if err != nil {
assert.NoError(t, fmt.Errorf("Error in mock response %v", err))
}
}
if req.URL.Path == "/repositories/test-owner/testmike/src/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/.gitignore" {
res.WriteHeader(http.StatusOK)
_, err := res.Write([]byte(`{
"mimetype": null,
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/src/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/.gitignore"
},
"meta": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/src/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/.gitignore?format=meta"
},
"history": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/filehistory/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/.gitignore"
}
},
"escaped_path": ".gitignore",
"path": ".gitignore",
"commit": {
"type": "commit",
"hash": "dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike/commits/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
}
}
},
"attributes": [],
"type": "commit_file",
"size": 624
}`))
if err != nil {
assert.NoError(t, fmt.Errorf("Error in mock response %v", err))
}
}
}))
defer func() { testServer.Close() }()
os.Setenv("BITBUCKET_API_BASE_URL", testServer.URL)
cases := []struct {
name, path, repo, owner, sha string
status int
}{
{
name: "exists",
owner: "test-owner",
repo: "testmike",
path: ".gitignore",
sha: "dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798",
status: http.StatusOK,
},
{
name: "not exists",
owner: "test-owner",
repo: "testmike",
path: ".gitignore2",
sha: "dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798",
status: http.StatusNotFound,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
provider, _ := NewBitBucketCloudProvider(context.Background(), c.owner, "user", "password", false)
repo := &Repository{
Organization: c.owner,
Repository: c.repo,
SHA: c.sha,
Branch: "main",
}
hasPath, err := provider.RepoHasPath(context.Background(), repo, c.path)
if err != nil {
assert.Error(t, fmt.Errorf("Error in test %v", err))
}
if c.status != http.StatusOK {
assert.False(t, hasPath)
} else {
assert.True(t, hasPath)
}
})
}
}
func TestBitbucketListRepos(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)
if req.URL.Path == "/repositories/test-owner/testmike/refs/branches" {
_, err := res.Write([]byte(`{
"pagelen": 10,
"values": [
{
"name": "main",
"links": {
"commits": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commits/main"
},
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/refs/branches/main"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike/branch/main"
}
},
"default_merge_strategy": "merge_commit",
"merge_strategies": [
"merge_commit",
"squash",
"fast_forward"
],
"type": "branch",
"target": {
"hash": "dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798",
"repository": {
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike"
},
"avatar": {
"href": "https://bytebucket.org/ravatar/%7B76606e75-8aeb-4a87-9396-4abee652ec63%7D?ts=default"
}
},
"type": "repository",
"name": "testMike",
"full_name": "test-owner/testmike",
"uuid": "{76606e75-8aeb-4a87-9396-4abee652ec63}"
},
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"comments": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/comments"
},
"patch": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/patch/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike/commits/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"diff": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/diff/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"approve": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/approve"
},
"statuses": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/statuses"
}
},
"author": {
"raw": "Mike Tester <tester@gmail.com>",
"type": "author",
"user": {
"display_name": "Mike Tester",
"uuid": "{ca84788f-050b-456b-5cac-93fb4484a686}",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7Bca84788f-050b-456b-5cac-93fb4484a686%7D"
},
"html": {
"href": "https://bitbucket.org/%7Bca84788f-050b-456b-5cac-93fb4484a686%7D/"
},
"avatar": {
"href": "https://secure.gravatar.com/avatar/03450fe11788d0dbb39b804110c07b9f?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMM-4.png"
}
},
"type": "user",
"nickname": "Mike Tester",
"account_id": "61ec57859d174000690f702b"
}
},
"parents": [],
"date": "2022-03-07T19:37:58+00:00",
"message": "Initial commit",
"type": "commit"
}
}
],
"page": 1,
"size": 1
}`))
if err != nil {
assert.NoError(t, fmt.Errorf("Error in mock response %v", err))
}
}
if req.URL.Path == "/repositories/test-owner/testmike/refs/branches/main" {
_, err := res.Write([]byte(`{
"name": "main",
"links": {
"commits": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commits/main"
},
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/refs/branches/main"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike/branch/main"
}
},
"default_merge_strategy": "merge_commit",
"merge_strategies": [
"merge_commit",
"squash",
"fast_forward"
],
"type": "branch",
"target": {
"hash": "dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798",
"repository": {
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike"
},
"avatar": {
"href": "https://bytebucket.org/ravatar/%7B76606e75-8aeb-4a87-9396-4abee652ec63%7D?ts=default"
}
},
"type": "repository",
"name": "testMike",
"full_name": "test-owner/testmike",
"uuid": "{76606e75-8aeb-4a87-9396-4abee652ec63}"
},
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"comments": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/comments"
},
"patch": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/patch/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike/commits/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"diff": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/diff/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798"
},
"approve": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/approve"
},
"statuses": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commit/dc1edb6c7d650d8ba67719ddf7b662ad8f8fb798/statuses"
}
},
"author": {
"raw": "Mike Tester <tester@gmail.com>",
"type": "author",
"user": {
"display_name": "Mike Tester",
"uuid": "{ca84788f-050b-456b-5cac-93fb4484a686}",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7Bca84788f-050b-456b-5cac-93fb4484a686%7D"
},
"html": {
"href": "https://bitbucket.org/%7Bca84788f-050b-456b-5cac-93fb4484a686%7D/"
},
"avatar": {
"href": "https://secure.gravatar.com/avatar/03450fe11788d0dbb39b804110c07b9f?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMM-4.png"
}
},
"type": "user",
"nickname": "Mike Tester",
"account_id": "61ec57859d174000690f702b"
}
},
"parents": [],
"date": "2022-03-07T19:37:58+00:00",
"message": "Initial commit",
"type": "commit"
}
}`))
if err != nil {
assert.NoError(t, fmt.Errorf("Error in mock response %v", err))
}
}
if req.URL.Path == "/repositories/test-owner" {
_, err := res.Write([]byte(`{
"pagelen": 10,
"values": [
{
"scm": "git",
"has_wiki": false,
"links": {
"watchers": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/watchers"
},
"branches": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/refs/branches"
},
"tags": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/refs/tags"
},
"commits": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/commits"
},
"clone": [
{
"href": "https://test-owner@bitbucket.org/test-owner/testmike.git",
"name": "https"
},
{
"href": "git@bitbucket.org:test-owner/testmike.git",
"name": "ssh"
}
],
"self": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike"
},
"source": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/src"
},
"html": {
"href": "https://bitbucket.org/test-owner/testmike"
},
"avatar": {
"href": "https://bytebucket.org/ravatar/%7B76606e75-8aeb-4a87-9396-4abee652ec63%7D?ts=default"
},
"hooks": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/hooks"
},
"forks": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/forks"
},
"downloads": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/downloads"
},
"pullrequests": {
"href": "https://api.bitbucket.org/2.0/repositories/test-owner/testmike/pullrequests"
}
},
"created_on": "2022-03-07T19:37:58.199968+00:00",
"full_name": "test-owner/testmike",
"owner": {
"display_name": "Mike Tester",
"uuid": "{ca84788f-050b-456b-5cac-93fb4484a686}",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/users/%7Bca84788f-050b-456b-5cac-93fb4484a686%7D"
},
"html": {
"href": "https://bitbucket.org/%7Bca84788f-050b-456b-5cac-93fb4484a686%7D/"
},
"avatar": {
"href": "https://secure.gravatar.com/avatar/03450fe11788d0dbb39b804110c07b9f?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FMM-4.png"
}
},
"type": "user",
"nickname": "Mike Tester",
"account_id": "61ec57859d174000690f702b"
},
"size": 58894,
"uuid": "{76606e75-8aeb-4a87-9396-4abee652ec63}",
"type": "repository",
"website": null,
"override_settings": {
"branching_model": true,
"default_merge_strategy": true,
"branch_restrictions": true
},
"description": "",
"has_issues": false,
"slug": "testmike",
"is_private": false,
"name": "testMike",
"language": "",
"fork_policy": "allow_forks",
"project": {
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/workspaces/test-owner/projects/TEST"
},
"html": {
"href": "https://bitbucket.org/test-owner/workspace/projects/TEST"
},
"avatar": {
"href": "https://bitbucket.org/account/user/test-owner/projects/TEST/avatar/32?ts=1642881431"
}
},
"type": "project",
"name": "test",
"key": "TEST",
"uuid": "{603a1564-1509-4c97-b2a6-300a3fad2758}"
},
"mainbranch": {
"type": "branch",
"name": "main"
},
"workspace": {
"slug": "test-owner",
"type": "workspace",
"name": "Mike Tester",
"links": {
"self": {
"href": "https://api.bitbucket.org/2.0/workspaces/test-owner"
},
"html": {
"href": "https://bitbucket.org/test-owner/"
},
"avatar": {
"href": "https://bitbucket.org/workspaces/test-owner/avatar/?ts=1642878863"
}
},
"uuid": "{ca84788f-050b-456b-5cac-93fb4484a686}"
},
"updated_on": "2022-03-07T19:37:59.933133+00:00"
}
],
"page": 1,
"size": 1
}`))
if err != nil {
assert.NoError(t, fmt.Errorf("Error in mock response %v", err))
}
}
}))
defer func() { testServer.Close() }()
os.Setenv("BITBUCKET_API_BASE_URL", testServer.URL)
cases := []struct {
name, proto, owner string
hasError, allBranches bool
branches []string
filters []v1alpha1.SCMProviderGeneratorFilter
}{
{
name: "blank protocol",
owner: "test-owner",
branches: []string{"main"},
},
{
name: "ssh protocol",
proto: "ssh",
owner: "test-owner",
},
{
name: "https protocol",
proto: "https",
owner: "test-owner",
},
{
name: "other protocol",
proto: "other",
owner: "test-owner",
hasError: true,
},
{
name: "all branches",
allBranches: true,
owner: "test-owner",
branches: []string{"main"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
provider, _ := NewBitBucketCloudProvider(context.Background(), c.owner, "user", "password", c.allBranches)
rawRepos, err := ListRepos(context.Background(), provider, c.filters, c.proto)
if c.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
repos := []*Repository{}
branches := []string{}
for _, r := range rawRepos {
if r.Repository == "testmike" {
repos = append(repos, r)
branches = append(branches, r.Branch)
}
}
assert.NotEmpty(t, repos)
for _, b := range c.branches {
assert.Contains(t, branches, b)
}
}
})
}
}

View file

@ -0,0 +1,155 @@
package scm_provider
import (
"context"
"errors"
"fmt"
"os"
"github.com/google/go-github/v35/github"
"golang.org/x/oauth2"
)
type GithubProvider struct {
client *github.Client
organization string
allBranches bool
}
var _ SCMProviderService = &GithubProvider{}
func NewGithubProvider(ctx context.Context, organization string, token string, url string, allBranches bool) (*GithubProvider, error) {
var ts oauth2.TokenSource
// Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
if token == "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token != "" {
ts = oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
}
httpClient := oauth2.NewClient(ctx, ts)
var client *github.Client
if url == "" {
client = github.NewClient(httpClient)
} else {
var err error
client, err = github.NewEnterpriseClient(url, url, httpClient)
if err != nil {
return nil, err
}
}
return &GithubProvider{client: client, organization: organization, allBranches: allBranches}, nil
}
func (g *GithubProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
repos := []*Repository{}
branches, err := g.listBranches(ctx, repo)
if err != nil {
return nil, fmt.Errorf("error listing branches for %s/%s: %v", repo.Organization, repo.Repository, err)
}
for _, branch := range branches {
repos = append(repos, &Repository{
Organization: repo.Organization,
Repository: repo.Repository,
URL: repo.URL,
Branch: branch.GetName(),
SHA: branch.GetCommit().GetSHA(),
Labels: repo.Labels,
RepositoryId: repo.RepositoryId,
})
}
return repos, nil
}
func (g *GithubProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
opt := &github.RepositoryListByOrgOptions{
ListOptions: github.ListOptions{PerPage: 100},
}
repos := []*Repository{}
for {
githubRepos, resp, err := g.client.Repositories.ListByOrg(ctx, g.organization, opt)
if err != nil {
return nil, fmt.Errorf("error listing repositories for %s: %v", g.organization, err)
}
for _, githubRepo := range githubRepos {
var url string
switch cloneProtocol {
// Default to SSH if unspecified (i.e. if "").
case "", "ssh":
url = githubRepo.GetSSHURL()
case "https":
url = githubRepo.GetCloneURL()
default:
return nil, fmt.Errorf("unknown clone protocol for GitHub %v", cloneProtocol)
}
repos = append(repos, &Repository{
Organization: githubRepo.Owner.GetLogin(),
Repository: githubRepo.GetName(),
Branch: githubRepo.GetDefaultBranch(),
URL: url,
Labels: githubRepo.Topics,
RepositoryId: githubRepo.ID,
})
}
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return repos, nil
}
func (g *GithubProvider) RepoHasPath(ctx context.Context, repo *Repository, path string) (bool, error) {
_, _, resp, err := g.client.Repositories.GetContents(ctx, repo.Organization, repo.Repository, path, &github.RepositoryContentGetOptions{
Ref: repo.Branch,
})
// 404s are not an error here, just a normal false.
if resp != nil && resp.StatusCode == 404 {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func (g *GithubProvider) listBranches(ctx context.Context, repo *Repository) ([]github.Branch, error) {
// If we don't specifically want to query for all branches, just use the default branch and call it a day.
if !g.allBranches {
defaultBranch, _, err := g.client.Repositories.GetBranch(ctx, repo.Organization, repo.Repository, repo.Branch)
if err != nil {
var githubErrorResponse *github.ErrorResponse
if errors.As(err, &githubErrorResponse) {
if githubErrorResponse.Response.StatusCode == 404 {
// Default branch doesn't exist, so the repo is empty.
return []github.Branch{}, nil
}
}
return nil, err
}
return []github.Branch{*defaultBranch}, nil
}
// Otherwise, scrape the ListBranches API.
opt := &github.BranchListOptions{
ListOptions: github.ListOptions{PerPage: 100},
}
branches := []github.Branch{}
for {
githubBranches, resp, err := g.client.Repositories.ListBranches(ctx, repo.Organization, repo.Repository, opt)
if err != nil {
return nil, err
}
for _, githubBranch := range githubBranches {
branches = append(branches, *githubBranch)
}
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return branches, nil
}

View file

@ -0,0 +1,117 @@
package scm_provider
import (
"context"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func checkRateLimit(t *testing.T, err error) {
// Check if we've hit a rate limit, don't fail the test if so.
if err != nil && (strings.Contains(err.Error(), "rate limit exceeded") ||
(strings.Contains(err.Error(), "API rate limit") && strings.Contains(err.Error(), "still exceeded"))) {
// GitHub Actions add this environment variable to indicate branch ref you are running on
githubRef := os.Getenv("GITHUB_REF")
// Only report rate limit errors as errors, when:
// - We are running in a GitHub action
// - AND, we are running that action on the 'master' or 'release-*' branch
// (unfortunately, for PRs, we don't have access to GitHub secrets that would allow us to embed a token)
failOnRateLimitErrors := os.Getenv("CI") != "" && (strings.Contains(githubRef, "/master") || strings.Contains(githubRef, "/release-"))
t.Logf("Got a rate limit error, consider setting $GITHUB_TOKEN to increase your GitHub API rate limit: %v\n", err)
if failOnRateLimitErrors {
t.FailNow()
} else {
t.SkipNow()
}
}
}
func TestGithubListRepos(t *testing.T) {
cases := []struct {
name, proto, url string
hasError, allBranches bool
branches []string
filters []v1alpha1.SCMProviderGeneratorFilter
}{
{
name: "blank protocol",
url: "git@github.com:argoproj/applicationset.git",
branches: []string{"master"},
},
{
name: "ssh protocol",
proto: "ssh",
url: "git@github.com:argoproj/applicationset.git",
},
{
name: "https protocol",
proto: "https",
url: "https://github.com/argoproj/applicationset.git",
},
{
name: "other protocol",
proto: "other",
hasError: true,
},
{
name: "all branches",
allBranches: true,
url: "git@github.com:argoproj/applicationset.git",
branches: []string{"master", "release-0.1.0"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
provider, _ := NewGithubProvider(context.Background(), "argoproj", "", "", c.allBranches)
rawRepos, err := ListRepos(context.Background(), provider, c.filters, c.proto)
if c.hasError {
assert.Error(t, err)
} else {
checkRateLimit(t, err)
assert.NoError(t, err)
// Just check that this one project shows up. Not a great test but better thing nothing?
repos := []*Repository{}
branches := []string{}
for _, r := range rawRepos {
if r.Repository == "applicationset" {
repos = append(repos, r)
branches = append(branches, r.Branch)
}
}
assert.NotEmpty(t, repos)
assert.Equal(t, c.url, repos[0].URL)
for _, b := range c.branches {
assert.Contains(t, branches, b)
}
}
})
}
}
func TestGithubHasPath(t *testing.T) {
host, _ := NewGithubProvider(context.Background(), "argoproj", "", "", false)
repo := &Repository{
Organization: "argoproj",
Repository: "applicationset",
Branch: "master",
}
ok, err := host.RepoHasPath(context.Background(), repo, "pkg/")
checkRateLimit(t, err)
assert.Nil(t, err)
assert.True(t, ok)
ok, err = host.RepoHasPath(context.Background(), repo, "notathing/")
checkRateLimit(t, err)
assert.Nil(t, err)
assert.False(t, ok)
}

View file

@ -0,0 +1,151 @@
package scm_provider
import (
"context"
"fmt"
"os"
gitlab "github.com/xanzy/go-gitlab"
)
type GitlabProvider struct {
client *gitlab.Client
organization string
allBranches bool
includeSubgroups bool
}
var _ SCMProviderService = &GitlabProvider{}
func NewGitlabProvider(ctx context.Context, organization string, token string, url string, allBranches, includeSubgroups bool) (*GitlabProvider, error) {
// Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
if token == "" {
token = os.Getenv("GITLAB_TOKEN")
}
var client *gitlab.Client
if url == "" {
var err error
client, err = gitlab.NewClient(token)
if err != nil {
return nil, err
}
} else {
var err error
client, err = gitlab.NewClient(token, gitlab.WithBaseURL(url))
if err != nil {
return nil, err
}
}
return &GitlabProvider{client: client, organization: organization, allBranches: allBranches, includeSubgroups: includeSubgroups}, nil
}
func (g *GitlabProvider) GetBranches(ctx context.Context, repo *Repository) ([]*Repository, error) {
repos := []*Repository{}
branches, err := g.listBranches(ctx, repo)
if err != nil {
return nil, fmt.Errorf("error listing branches for %s/%s: %v", repo.Organization, repo.Repository, err)
}
for _, branch := range branches {
repos = append(repos, &Repository{
Organization: repo.Organization,
Repository: repo.Repository,
URL: repo.URL,
Branch: branch.Name,
SHA: branch.Commit.ID,
Labels: repo.Labels,
RepositoryId: repo.RepositoryId,
})
}
return repos, nil
}
func (g *GitlabProvider) ListRepos(ctx context.Context, cloneProtocol string) ([]*Repository, error) {
opt := &gitlab.ListGroupProjectsOptions{
ListOptions: gitlab.ListOptions{PerPage: 100},
IncludeSubgroups: &g.includeSubgroups,
}
repos := []*Repository{}
for {
gitlabRepos, resp, err := g.client.Groups.ListGroupProjects(g.organization, opt)
if err != nil {
return nil, fmt.Errorf("error listing projects for %s: %v", g.organization, err)
}
for _, gitlabRepo := range gitlabRepos {
var url string
switch cloneProtocol {
// Default to SSH if unspecified (i.e. if "").
case "", "ssh":
url = gitlabRepo.SSHURLToRepo
case "https":
url = gitlabRepo.HTTPURLToRepo
default:
return nil, fmt.Errorf("unknown clone protocol for Gitlab %v", cloneProtocol)
}
repos = append(repos, &Repository{
Organization: gitlabRepo.Namespace.FullPath,
Repository: gitlabRepo.Path,
URL: url,
Branch: gitlabRepo.DefaultBranch,
Labels: gitlabRepo.TagList,
RepositoryId: gitlabRepo.ID,
})
}
if resp.CurrentPage >= resp.TotalPages {
break
}
opt.Page = resp.NextPage
}
return repos, nil
}
func (g *GitlabProvider) RepoHasPath(_ context.Context, repo *Repository, path string) (bool, error) {
p, _, err := g.client.Projects.GetProject(repo.Organization+"/"+repo.Repository, nil)
if err != nil {
return false, err
}
_, resp, err := g.client.Repositories.ListTree(p.ID, &gitlab.ListTreeOptions{
Path: &path,
Ref: &repo.Branch,
})
if err != nil {
return false, err
}
if resp.TotalItems == 0 {
return false, nil
}
return true, nil
}
func (g *GitlabProvider) listBranches(_ context.Context, repo *Repository) ([]gitlab.Branch, error) {
branches := []gitlab.Branch{}
// If we don't specifically want to query for all branches, just use the default branch and call it a day.
if !g.allBranches {
gitlabBranch, _, err := g.client.Branches.GetBranch(repo.RepositoryId, repo.Branch, nil)
if err != nil {
return nil, err
}
branches = append(branches, *gitlabBranch)
return branches, nil
}
// Otherwise, scrape the ListBranches API.
opt := &gitlab.ListBranchesOptions{
ListOptions: gitlab.ListOptions{PerPage: 100},
}
for {
gitlabBranches, resp, err := g.client.Branches.ListBranches(repo.RepositoryId, opt)
if err != nil {
return nil, err
}
for _, gitlabBranch := range gitlabBranches {
branches = append(branches, *gitlabBranch)
}
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return branches, nil
}

View file

@ -0,0 +1,89 @@
package scm_provider
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestGitlabListRepos(t *testing.T) {
cases := []struct {
name, proto, url string
hasError, allBranches, includeSubgroups bool
branches []string
filters []v1alpha1.SCMProviderGeneratorFilter
}{
{
name: "blank protocol",
url: "git@gitlab.com:test-argocd-proton/argocd.git",
branches: []string{"master"},
},
{
name: "ssh protocol",
proto: "ssh",
url: "git@gitlab.com:test-argocd-proton/argocd.git",
},
{
name: "https protocol",
proto: "https",
url: "https://gitlab.com/test-argocd-proton/argocd.git",
},
{
name: "other protocol",
proto: "other",
hasError: true,
},
{
name: "all branches",
allBranches: true,
url: "git@gitlab.com:test-argocd-proton/argocd.git",
branches: []string{"master", "pipeline-1310077506"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
provider, _ := NewGitlabProvider(context.Background(), "test-argocd-proton", "", "", c.allBranches, c.includeSubgroups)
rawRepos, err := ListRepos(context.Background(), provider, c.filters, c.proto)
if c.hasError {
assert.NotNil(t, err)
} else {
checkRateLimit(t, err)
assert.Nil(t, err)
// Just check that this one project shows up. Not a great test but better thing nothing?
repos := []*Repository{}
branches := []string{}
for _, r := range rawRepos {
if r.Repository == "argocd" {
repos = append(repos, r)
branches = append(branches, r.Branch)
}
}
assert.NotEmpty(t, repos)
assert.Equal(t, c.url, repos[0].URL)
for _, b := range c.branches {
assert.Contains(t, branches, b)
}
}
})
}
}
func TestGitlabHasPath(t *testing.T) {
host, _ := NewGitlabProvider(context.Background(), "test-argocd-proton", "", "", false, true)
repo := &Repository{
Organization: "test-argocd-proton",
Repository: "argocd",
Branch: "master",
}
ok, err := host.RepoHasPath(context.Background(), repo, "argocd")
assert.Nil(t, err)
assert.True(t, ok)
ok, err = host.RepoHasPath(context.Background(), repo, "notathing")
assert.Nil(t, err)
assert.False(t, ok)
}

View file

@ -0,0 +1,50 @@
package scm_provider
import "context"
type MockProvider struct {
Repos []*Repository
}
var _ SCMProviderService = &MockProvider{}
func (m *MockProvider) ListRepos(_ context.Context, _ string) ([]*Repository, error) {
repos := []*Repository{}
for _, candidateRepo := range m.Repos {
found := false
for _, alreadySetRepo := range repos {
if alreadySetRepo.Repository == candidateRepo.Repository {
found = true
break
}
}
if !found {
repos = append(repos, candidateRepo)
}
}
return repos, nil
}
func (*MockProvider) RepoHasPath(_ context.Context, repo *Repository, path string) (bool, error) {
return path == repo.Repository, nil
}
func (m *MockProvider) GetBranches(_ context.Context, repo *Repository) ([]*Repository, error) {
branchRepos := []*Repository{}
for _, candidateRepo := range m.Repos {
if candidateRepo.Repository == repo.Repository {
found := false
for _, alreadySetRepo := range branchRepos {
if alreadySetRepo.Branch == candidateRepo.Branch {
found = true
break
}
}
if !found {
branchRepos = append(branchRepos, candidateRepo)
}
}
}
return branchRepos, nil
}

View file

@ -0,0 +1,42 @@
package scm_provider
import (
"context"
"regexp"
)
// An abstract repository from an API provider.
type Repository struct {
Organization string
Repository string
URL string
Branch string
SHA string
Labels []string
RepositoryId interface{}
}
type SCMProviderService interface {
ListRepos(context.Context, string) ([]*Repository, error)
RepoHasPath(context.Context, *Repository, string) (bool, error)
GetBranches(context.Context, *Repository) ([]*Repository, error)
}
// A compiled version of SCMProviderGeneratorFilter for performance.
type Filter struct {
RepositoryMatch *regexp.Regexp
PathsExist []string
LabelMatch *regexp.Regexp
BranchMatch *regexp.Regexp
FilterType FilterType
}
// A convenience type for indicating where to apply a filter
type FilterType int64
// The enum of filter types
const (
FilterTypeUndefined FilterType = iota
FilterTypeBranch
FilterTypeRepo
)

View file

@ -0,0 +1,167 @@
package scm_provider
import (
"context"
"fmt"
"regexp"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func compileFilters(filters []argoprojiov1alpha1.SCMProviderGeneratorFilter) ([]*Filter, error) {
outFilters := make([]*Filter, 0, len(filters))
for _, filter := range filters {
outFilter := &Filter{}
var err error
if filter.RepositoryMatch != nil {
outFilter.RepositoryMatch, err = regexp.Compile(*filter.RepositoryMatch)
if err != nil {
return nil, fmt.Errorf("error compiling RepositoryMatch regexp %q: %v", *filter.RepositoryMatch, err)
}
outFilter.FilterType = FilterTypeRepo
}
if filter.LabelMatch != nil {
outFilter.LabelMatch, err = regexp.Compile(*filter.LabelMatch)
if err != nil {
return nil, fmt.Errorf("error compiling LabelMatch regexp %q: %v", *filter.LabelMatch, err)
}
outFilter.FilterType = FilterTypeRepo
}
if filter.PathsExist != nil {
outFilter.PathsExist = filter.PathsExist
outFilter.FilterType = FilterTypeBranch
}
if filter.BranchMatch != nil {
outFilter.BranchMatch, err = regexp.Compile(*filter.BranchMatch)
if err != nil {
return nil, fmt.Errorf("error compiling BranchMatch regexp %q: %v", *filter.LabelMatch, err)
}
outFilter.FilterType = FilterTypeBranch
}
outFilters = append(outFilters, outFilter)
}
return outFilters, nil
}
func matchFilter(ctx context.Context, provider SCMProviderService, repo *Repository, filter *Filter) (bool, error) {
if filter.RepositoryMatch != nil && !filter.RepositoryMatch.MatchString(repo.Repository) {
return false, nil
}
if filter.BranchMatch != nil && !filter.BranchMatch.MatchString(repo.Branch) {
return false, nil
}
if filter.LabelMatch != nil {
found := false
for _, label := range repo.Labels {
if filter.LabelMatch.MatchString(label) {
found = true
break
}
}
if !found {
return false, nil
}
}
if len(filter.PathsExist) != 0 {
for _, path := range filter.PathsExist {
hasPath, err := provider.RepoHasPath(ctx, repo, path)
if err != nil {
return false, err
}
if !hasPath {
return false, nil
}
}
}
return true, nil
}
func ListRepos(ctx context.Context, provider SCMProviderService, filters []argoprojiov1alpha1.SCMProviderGeneratorFilter, cloneProtocol string) ([]*Repository, error) {
compiledFilters, err := compileFilters(filters)
if err != nil {
return nil, err
}
repos, err := provider.ListRepos(ctx, cloneProtocol)
if err != nil {
return nil, err
}
repoFilters := getApplicableFilters(compiledFilters)[FilterTypeRepo]
if len(repoFilters) == 0 {
repos, err := getBranches(ctx, provider, repos, compiledFilters)
if err != nil {
return nil, err
}
return repos, nil
}
filteredRepos := make([]*Repository, 0, len(repos))
for _, repo := range repos {
for _, filter := range repoFilters {
matches, err := matchFilter(ctx, provider, repo, filter)
if err != nil {
return nil, err
}
if matches {
filteredRepos = append(filteredRepos, repo)
break
}
}
}
repos, err = getBranches(ctx, provider, filteredRepos, compiledFilters)
if err != nil {
return nil, err
}
return repos, nil
}
func getBranches(ctx context.Context, provider SCMProviderService, repos []*Repository, compiledFilters []*Filter) ([]*Repository, error) {
reposWithBranches := []*Repository{}
for _, repo := range repos {
reposFilled, err := provider.GetBranches(ctx, repo)
if err != nil {
return nil, err
}
reposWithBranches = append(reposWithBranches, reposFilled...)
}
branchFilters := getApplicableFilters(compiledFilters)[FilterTypeBranch]
if len(branchFilters) == 0 {
return reposWithBranches, nil
}
filteredRepos := make([]*Repository, 0, len(reposWithBranches))
for _, repo := range reposWithBranches {
for _, filter := range branchFilters {
matches, err := matchFilter(ctx, provider, repo, filter)
if err != nil {
return nil, err
}
if matches {
filteredRepos = append(filteredRepos, repo)
break
}
}
}
return filteredRepos, nil
}
// getApplicableFilters returns a map of filters separated by type.
func getApplicableFilters(filters []*Filter) map[FilterType][]*Filter {
filterMap := map[FilterType][]*Filter{
FilterTypeBranch: {},
FilterTypeRepo: {},
}
for _, filter := range filters {
if filter.FilterType == FilterTypeBranch {
filterMap[FilterTypeBranch] = append(filterMap[FilterTypeBranch], filter)
} else if filter.FilterType == FilterTypeRepo {
filterMap[FilterTypeRepo] = append(filterMap[FilterTypeRepo], filter)
}
}
return filterMap
}

View file

@ -0,0 +1,292 @@
package scm_provider
import (
"context"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func strp(s string) *string {
return &s
}
func TestFilterRepoMatch(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
},
{
Repository: "two",
},
{
Repository: "three",
},
{
Repository: "four",
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
RepositoryMatch: strp("n|hr"),
},
}
repos, err := ListRepos(context.Background(), provider, filters, "")
assert.Nil(t, err)
assert.Len(t, repos, 2)
assert.Equal(t, "one", repos[0].Repository)
assert.Equal(t, "three", repos[1].Repository)
}
func TestFilterLabelMatch(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
Labels: []string{"prod-one", "prod-two", "staging"},
},
{
Repository: "two",
Labels: []string{"prod-two"},
},
{
Repository: "three",
Labels: []string{"staging"},
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
LabelMatch: strp("^prod-.*$"),
},
}
repos, err := ListRepos(context.Background(), provider, filters, "")
assert.Nil(t, err)
assert.Len(t, repos, 2)
assert.Equal(t, "one", repos[0].Repository)
assert.Equal(t, "two", repos[1].Repository)
}
func TestFilterPatchExists(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
},
{
Repository: "two",
},
{
Repository: "three",
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
PathsExist: []string{"two"},
},
}
repos, err := ListRepos(context.Background(), provider, filters, "")
assert.Nil(t, err)
assert.Len(t, repos, 1)
assert.Equal(t, "two", repos[0].Repository)
}
func TestFilterRepoMatchBadRegexp(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
RepositoryMatch: strp("("),
},
}
_, err := ListRepos(context.Background(), provider, filters, "")
assert.NotNil(t, err)
}
func TestFilterLabelMatchBadRegexp(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
LabelMatch: strp("("),
},
}
_, err := ListRepos(context.Background(), provider, filters, "")
assert.NotNil(t, err)
}
func TestFilterBranchMatch(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
Branch: "one",
},
{
Repository: "one",
Branch: "two",
},
{
Repository: "two",
Branch: "one",
},
{
Repository: "three",
Branch: "one",
},
{
Repository: "three",
Branch: "two",
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
BranchMatch: strp("w"),
},
}
repos, err := ListRepos(context.Background(), provider, filters, "")
assert.Nil(t, err)
assert.Len(t, repos, 2)
assert.Equal(t, "one", repos[0].Repository)
assert.Equal(t, "two", repos[0].Branch)
assert.Equal(t, "three", repos[1].Repository)
assert.Equal(t, "two", repos[1].Branch)
}
func TestMultiFilterAnd(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
Labels: []string{"prod-one", "prod-two", "staging"},
},
{
Repository: "two",
Labels: []string{"prod-two"},
},
{
Repository: "three",
Labels: []string{"staging"},
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
RepositoryMatch: strp("w"),
LabelMatch: strp("^prod-.*$"),
},
}
repos, err := ListRepos(context.Background(), provider, filters, "")
assert.Nil(t, err)
assert.Len(t, repos, 1)
assert.Equal(t, "two", repos[0].Repository)
}
func TestMultiFilterOr(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
Labels: []string{"prod-one", "prod-two", "staging"},
},
{
Repository: "two",
Labels: []string{"prod-two"},
},
{
Repository: "three",
Labels: []string{"staging"},
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{
{
RepositoryMatch: strp("e"),
},
{
LabelMatch: strp("^prod-.*$"),
},
}
repos, err := ListRepos(context.Background(), provider, filters, "")
assert.Nil(t, err)
assert.Len(t, repos, 3)
assert.Equal(t, "one", repos[0].Repository)
assert.Equal(t, "two", repos[1].Repository)
assert.Equal(t, "three", repos[2].Repository)
}
func TestNoFilters(t *testing.T) {
provider := &MockProvider{
Repos: []*Repository{
{
Repository: "one",
Labels: []string{"prod-one", "prod-two", "staging"},
},
{
Repository: "two",
Labels: []string{"prod-two"},
},
{
Repository: "three",
Labels: []string{"staging"},
},
},
}
filters := []argoprojiov1alpha1.SCMProviderGeneratorFilter{}
repos, err := ListRepos(context.Background(), provider, filters, "")
assert.Nil(t, err)
assert.Len(t, repos, 3)
assert.Equal(t, "one", repos[0].Repository)
assert.Equal(t, "two", repos[1].Repository)
assert.Equal(t, "three", repos[2].Repository)
}
// tests the getApplicableFilters function, passing in all the filters, and an unset filter, plus an additional
// branch filter
func TestApplicableFilterMap(t *testing.T) {
branchFilter := Filter{
BranchMatch: &regexp.Regexp{},
FilterType: FilterTypeBranch,
}
repoFilter := Filter{
RepositoryMatch: &regexp.Regexp{},
FilterType: FilterTypeRepo,
}
pathExistsFilter := Filter{
PathsExist: []string{"test"},
FilterType: FilterTypeBranch,
}
labelMatchFilter := Filter{
LabelMatch: &regexp.Regexp{},
FilterType: FilterTypeRepo,
}
unsetFilter := Filter{
LabelMatch: &regexp.Regexp{},
}
additionalBranchFilter := Filter{
BranchMatch: &regexp.Regexp{},
FilterType: FilterTypeBranch,
}
filterMap := getApplicableFilters([]*Filter{&branchFilter, &repoFilter,
&pathExistsFilter, &labelMatchFilter, &unsetFilter, &additionalBranchFilter})
assert.Len(t, filterMap[FilterTypeRepo], 2)
assert.Len(t, filterMap[FilterTypeBranch], 3)
}

View file

@ -0,0 +1,196 @@
package utils
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v2/common"
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"k8s.io/client-go/kubernetes"
"k8s.io/utils/pointer"
)
// The contents of this file are from
// github.com/argoproj/argo-cd/util/db/cluster.go
//
// The main difference is that ListClusters(...) calls the kubeclient directly,
// via `g.clientset.CoreV1().Secrets`, rather than using the `db.listClusterSecrets()``
// which appears to have a race condition on when it is called.
//
// I was reminded of this issue that I opened, which might be related:
// https://github.com/argoproj/argo-cd/issues/4755
//
// I hope to upstream this change in some form, so that we do not need to worry about
// Argo CD changing the logic on us.
var (
localCluster = appv1.Cluster{
Name: "in-cluster",
Server: appv1.KubernetesInternalAPIServerAddr,
ConnectionState: appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful},
}
initLocalCluster sync.Once
)
const (
ArgoCDSecretTypeLabel = "argocd.argoproj.io/secret-type"
ArgoCDSecretTypeCluster = "cluster"
)
// ValidateDestination checks:
// if we used destination name we infer the server url
// if we used both name and server then we return an invalid spec error
func ValidateDestination(ctx context.Context, dest *appv1.ApplicationDestination, clientset kubernetes.Interface, namespace string) error {
if dest.Name != "" {
if dest.Server == "" {
server, err := getDestinationServer(ctx, dest.Name, clientset, namespace)
if err != nil {
return fmt.Errorf("unable to find destination server: %v", err)
}
if server == "" {
return fmt.Errorf("application references destination cluster %s which does not exist", dest.Name)
}
dest.SetInferredServer(server)
} else {
if !dest.IsServerInferred() {
return fmt.Errorf("application destination can't have both name and server defined: %s %s", dest.Name, dest.Server)
}
}
}
return nil
}
func getDestinationServer(ctx context.Context, clusterName string, clientset kubernetes.Interface, namespace string) (string, error) {
// settingsMgr := settings.NewSettingsManager(context.TODO(), clientset, namespace)
// argoDB := db.NewDB(namespace, settingsMgr, clientset)
// clusterList, err := argoDB.ListClusters(ctx)
clusterList, err := ListClusters(ctx, clientset, namespace)
if err != nil {
return "", err
}
var servers []string
for _, c := range clusterList.Items {
if c.Name == clusterName {
servers = append(servers, c.Server)
}
}
if len(servers) > 1 {
return "", fmt.Errorf("there are %d clusters with the same name: %v", len(servers), servers)
} else if len(servers) == 0 {
return "", fmt.Errorf("there are no clusters with this name: %s", clusterName)
}
return servers[0], nil
}
func ListClusters(ctx context.Context, clientset kubernetes.Interface, namespace string) (*appv1.ClusterList, error) {
clusterSecretsList, err := clientset.CoreV1().Secrets(namespace).List(ctx,
metav1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeCluster})
if err != nil {
return nil, err
}
if clusterSecretsList == nil {
return nil, nil
}
clusterSecrets := clusterSecretsList.Items
clusterList := appv1.ClusterList{
Items: make([]appv1.Cluster, len(clusterSecrets)),
}
hasInClusterCredentials := false
for i, clusterSecret := range clusterSecrets {
// This line has changed from the original Argo CD code: now receives an error, and handles it
cluster, err := secretToCluster(&clusterSecret)
if err != nil || cluster == nil {
return nil, fmt.Errorf("unable to convert cluster secret to cluster object '%s': %v", clusterSecret.Name, err)
}
clusterList.Items[i] = *cluster
if cluster.Server == appv1.KubernetesInternalAPIServerAddr {
hasInClusterCredentials = true
}
}
if !hasInClusterCredentials {
localCluster := getLocalCluster(clientset)
if localCluster != nil {
clusterList.Items = append(clusterList.Items, *localCluster)
}
}
return &clusterList, nil
}
func getLocalCluster(clientset kubernetes.Interface) *appv1.Cluster {
initLocalCluster.Do(func() {
info, err := clientset.Discovery().ServerVersion()
if err == nil {
localCluster.ServerVersion = fmt.Sprintf("%s.%s", info.Major, info.Minor)
localCluster.ConnectionState = appv1.ConnectionState{Status: appv1.ConnectionStatusSuccessful}
} else {
localCluster.ConnectionState = appv1.ConnectionState{
Status: appv1.ConnectionStatusFailed,
Message: err.Error(),
}
}
})
cluster := localCluster.DeepCopy()
now := metav1.Now()
cluster.ConnectionState.ModifiedAt = &now
return cluster
}
// secretToCluster converts a secret into a Cluster object
func secretToCluster(s *corev1.Secret) (*appv1.Cluster, error) {
var config appv1.ClusterConfig
if len(s.Data["config"]) > 0 {
if err := json.Unmarshal(s.Data["config"], &config); err != nil {
// This line has changed from the original Argo CD: now returns an error rather than panicing.
return nil, err
}
}
var namespaces []string
for _, ns := range strings.Split(string(s.Data["namespaces"]), ",") {
if ns = strings.TrimSpace(ns); ns != "" {
namespaces = append(namespaces, ns)
}
}
var refreshRequestedAt *metav1.Time
if v, found := s.Annotations[appv1.AnnotationKeyRefresh]; found {
requestedAt, err := time.Parse(time.RFC3339, v)
if err != nil {
log.Warnf("Error while parsing date in cluster secret '%s': %v", s.Name, err)
} else {
refreshRequestedAt = &metav1.Time{Time: requestedAt}
}
}
var shard *int64
if shardStr := s.Data["shard"]; shardStr != nil {
if val, err := strconv.Atoi(string(shardStr)); err != nil {
log.Warnf("Error while parsing shard in cluster secret '%s': %v", s.Name, err)
} else {
shard = pointer.Int64Ptr(int64(val))
}
}
cluster := appv1.Cluster{
ID: string(s.UID),
Server: strings.TrimRight(string(s.Data["server"]), "/"),
Name: string(s.Data["name"]),
Namespaces: namespaces,
Config: config,
RefreshRequestedAt: refreshRequestedAt,
Shard: shard,
}
return &cluster, nil
}

View file

@ -0,0 +1,179 @@
package utils
import (
"context"
"fmt"
"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/client-go/kubernetes/fake"
kubetesting "k8s.io/client-go/testing"
argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
const (
fakeNamespace = "fake-ns"
)
// From Argo CD util/db/cluster_test.go
func Test_secretToCluster(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mycluster",
Namespace: fakeNamespace,
},
Data: map[string][]byte{
"name": []byte("test"),
"server": []byte("http://mycluster"),
"config": []byte("{\"username\":\"foo\"}"),
},
}
cluster, err := secretToCluster(secret)
assert.Nil(t, err)
assert.Equal(t, *cluster, argoappv1.Cluster{
Name: "test",
Server: "http://mycluster",
Config: argoappv1.ClusterConfig{
Username: "foo",
},
})
}
// From Argo CD util/db/cluster_test.go
func Test_secretToCluster_NoConfig(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mycluster",
Namespace: fakeNamespace,
},
Data: map[string][]byte{
"name": []byte("test"),
"server": []byte("http://mycluster"),
},
}
cluster, err := secretToCluster(secret)
assert.Nil(t, err)
assert.Equal(t, *cluster, argoappv1.Cluster{
Name: "test",
Server: "http://mycluster",
})
}
func createClusterSecret(secretName string, clusterName string, clusterServer string) *corev1.Secret {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: utils.ArgoCDNamespace,
Labels: map[string]string{
ArgoCDSecretTypeLabel: ArgoCDSecretTypeCluster,
},
},
Data: map[string][]byte{
"name": []byte(clusterName),
"server": []byte(clusterServer),
"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
},
}
return secret
}
// From util/argo/argo_test.go
// (ported to use kubeclientset)
func TestValidateDestination(t *testing.T) {
t.Run("Validate destination with server url", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Namespace: "default",
}
appCond := ValidateDestination(context.Background(), &dest, nil, fakeNamespace)
assert.Nil(t, appCond)
assert.False(t, dest.IsServerInferred())
})
t.Run("Validate destination with server name", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "minikube",
}
secret := createClusterSecret("my-secret", "minikube", "https://127.0.0.1:6443")
objects := []runtime.Object{}
objects = append(objects, secret)
kubeclientset := fake.NewSimpleClientset(objects...)
appCond := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
assert.Nil(t, appCond)
assert.Equal(t, "https://127.0.0.1:6443", dest.Server)
assert.True(t, dest.IsServerInferred())
})
t.Run("Error when having both server url and name", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Server: "https://127.0.0.1:6443",
Name: "minikube",
Namespace: "default",
}
err := ValidateDestination(context.Background(), &dest, nil, utils.ArgoCDNamespace)
assert.Equal(t, "application destination can't have both name and server defined: minikube https://127.0.0.1:6443", err.Error())
assert.False(t, dest.IsServerInferred())
})
t.Run("List clusters fails", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "minikube",
}
kubeclientset := fake.NewSimpleClientset()
kubeclientset.PrependReactor("list", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, fmt.Errorf("an error occurred")
})
err := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
assert.Equal(t, "unable to find destination server: an error occurred", err.Error())
assert.False(t, dest.IsServerInferred())
})
t.Run("Destination cluster does not exist", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "minikube",
}
secret := createClusterSecret("dind", "dind", "https://127.0.0.1:6443")
objects := []runtime.Object{}
objects = append(objects, secret)
kubeclientset := fake.NewSimpleClientset(objects...)
err := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
assert.Equal(t, "unable to find destination server: there are no clusters with this name: minikube", err.Error())
assert.False(t, dest.IsServerInferred())
})
t.Run("Validate too many clusters with the same name", func(t *testing.T) {
dest := argoappv1.ApplicationDestination{
Name: "dind",
}
secret := createClusterSecret("dind", "dind", "https://127.0.0.1:2443")
secret2 := createClusterSecret("dind2", "dind", "https://127.0.0.1:8443")
objects := []runtime.Object{}
objects = append(objects, secret, secret2)
kubeclientset := fake.NewSimpleClientset(objects...)
err := ValidateDestination(context.Background(), &dest, kubeclientset, utils.ArgoCDNamespace)
assert.Equal(t, "unable to find destination server: there are 2 clusters with the same name: [https://127.0.0.1:2443 https://127.0.0.1:8443]", err.Error())
assert.False(t, dest.IsServerInferred())
})
}

View file

@ -0,0 +1,97 @@
package utils
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
// CreateOrUpdate overrides "sigs.k8s.io/controller-runtime" function
// in sigs.k8s.io/controller-runtime/pkg/controller/controllerutil/controllerutil.go
// to add equality for argov1alpha1.ApplicationDestination
// argov1alpha1.ApplicationDestination has a private variable, so the default
// implementation fails to compare it.
//
// CreateOrUpdate creates or updates the given object in the Kubernetes
// cluster. The object's desired state must be reconciled with the existing
// state inside the passed in callback MutateFn.
//
// The MutateFn is called regardless of creating or updating an object.
//
// It returns the executed operation and an error.
func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
key := client.ObjectKeyFromObject(obj)
if err := c.Get(ctx, key, obj); err != nil {
if !errors.IsNotFound(err) {
return controllerutil.OperationResultNone, err
}
if err := mutate(f, key, obj); err != nil {
return controllerutil.OperationResultNone, err
}
if err := c.Create(ctx, obj); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultCreated, nil
}
existing := obj.DeepCopyObject()
if err := mutate(f, key, obj); err != nil {
return controllerutil.OperationResultNone, err
}
equality := conversion.EqualitiesOrDie(
func(a, b resource.Quantity) bool {
// Ignore formatting, only care that numeric value stayed the same.
// TODO: if we decide it's important, it should be safe to start comparing the format.
//
// Uninitialized quantities are equivalent to 0 quantities.
return a.Cmp(b) == 0
},
func(a, b metav1.MicroTime) bool {
return a.UTC() == b.UTC()
},
func(a, b metav1.Time) bool {
return a.UTC() == b.UTC()
},
func(a, b labels.Selector) bool {
return a.String() == b.String()
},
func(a, b fields.Selector) bool {
return a.String() == b.String()
},
func(a, b argov1alpha1.ApplicationDestination) bool {
return a.Namespace == b.Namespace && a.Name == b.Name && a.Server == b.Server
},
)
if equality.DeepEqual(existing, obj) {
return controllerutil.OperationResultNone, nil
}
if err := c.Update(ctx, obj); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultUpdated, nil
}
// mutate wraps a MutateFn and applies validation to its result
func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object) error {
if err := f(); err != nil {
return err
}
if newKey := client.ObjectKeyFromObject(obj); key != newKey {
return fmt.Errorf("MutateFn cannot mutate object name and/or object namespace")
}
return nil
}

View file

@ -0,0 +1,38 @@
package utils
import (
"fmt"
)
func CombineStringMaps(a map[string]string, b map[string]string) (map[string]string, error) {
res := map[string]string{}
for k, v := range a {
res[k] = v
}
for k, v := range b {
current, present := res[k]
if present && current != v {
return nil, fmt.Errorf("found duplicate key %s with different value, a: %s ,b: %s", k, current, v)
}
res[k] = v
}
return res, nil
}
// CombineStringMapsAllowDuplicates merges two maps. Where there are duplicates, take the latter map's value.
func CombineStringMapsAllowDuplicates(a map[string]string, b map[string]string) (map[string]string, error) {
res := map[string]string{}
for k, v := range a {
res[k] = v
}
for k, v := range b {
res[k] = v
}
return res, nil
}

View file

@ -0,0 +1,58 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCombineStringMaps(t *testing.T) {
testCases := []struct {
name string
left map[string]string
right map[string]string
expected map[string]string
expectedErr error
}{
{
name: "combines the maps",
left: map[string]string{"foo": "bar"},
right: map[string]string{"a": "b"},
expected: map[string]string{"a": "b", "foo": "bar"},
expectedErr: nil,
},
{
name: "fails if keys are the same but value isn't",
left: map[string]string{"foo": "bar", "a": "fail"},
right: map[string]string{"a": "b", "c": "d"},
expected: map[string]string{"a": "b", "foo": "bar"},
expectedErr: fmt.Errorf("found duplicate key a with different value, a: fail ,b: b"),
},
{
name: "pass if keys & values are the same",
left: map[string]string{"foo": "bar", "a": "b"},
right: map[string]string{"a": "b", "c": "d"},
expected: map[string]string{"a": "b", "c": "d", "foo": "bar"},
expectedErr: nil,
},
}
for _, testCase := range testCases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
got, err := CombineStringMaps(testCaseCopy.left, testCaseCopy.right)
if testCaseCopy.expectedErr != nil {
assert.EqualError(t, err, testCaseCopy.expectedErr.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, testCaseCopy.expected, got)
}
})
}
}

View file

@ -0,0 +1,44 @@
package utils
// Policy allows to apply different rules to a set of changes.
type Policy interface {
Update() bool
Delete() bool
}
// Policies is a registry of available policies.
var Policies = map[string]Policy{
"sync": &SyncPolicy{},
"create-only": &CreateOnlyPolicy{},
"create-update": &CreateUpdatePolicy{},
}
type SyncPolicy struct{}
func (p *SyncPolicy) Update() bool {
return true
}
func (p *SyncPolicy) Delete() bool {
return true
}
type CreateUpdatePolicy struct{}
func (p *CreateUpdatePolicy) Update() bool {
return true
}
func (p *CreateUpdatePolicy) Delete() bool {
return false
}
type CreateOnlyPolicy struct{}
func (p *CreateOnlyPolicy) Update() bool {
return false
}
func (p *CreateOnlyPolicy) Delete() bool {
return false
}

View file

@ -0,0 +1,186 @@
{
"ref": "refs/heads/master",
"before": "d5c1ffa8e294bc18c639bfb4e0df499251034414",
"after": "63738bb582c8b540af7bcfc18f87c575c3ed66e0",
"created": false,
"deleted": false,
"forced": true,
"base_ref": null,
"compare": "https://github.com/org/repo/compare/d5c1ffa8e294...63738bb582c8",
"commits": [
{
"id": "63738bb582c8b540af7bcfc18f87c575c3ed66e0",
"tree_id": "64897da445207e409ad05af93b1f349ad0a4ee19",
"distinct": true,
"message": "Add staging-argocd-demo environment",
"timestamp": "2018-05-04T15:40:02-07:00",
"url": "https://github.com/org/repo/commit/63738bb582c8b540af7bcfc18f87c575c3ed66e0",
"author": {
"name": "Jesse Suen",
"email": "Jesse_Suen@example.com",
"username": "org"
},
"committer": {
"name": "Jesse Suen",
"email": "Jesse_Suen@example.com",
"username": "org"
},
"added": [
"ksapps/test-app/environments/staging-argocd-demo/main.jsonnet",
"ksapps/test-app/environments/staging-argocd-demo/params.libsonnet"
],
"removed": [
],
"modified": [
"ksapps/test-app/app.yaml"
]
}
],
"head_commit": {
"id": "63738bb582c8b540af7bcfc18f87c575c3ed66e0",
"tree_id": "64897da445207e409ad05af93b1f349ad0a4ee19",
"distinct": true,
"message": "Add staging-argocd-demo environment",
"timestamp": "2018-05-04T15:40:02-07:00",
"url": "https://github.com/org/repo/commit/63738bb582c8b540af7bcfc18f87c575c3ed66e0",
"author": {
"name": "Jesse Suen",
"email": "Jesse_Suen@example.com",
"username": "org"
},
"committer": {
"name": "Jesse Suen",
"email": "Jesse_Suen@example.com",
"username": "org"
},
"added": [
"ksapps/test-app/environments/staging-argocd-demo/main.jsonnet",
"ksapps/test-app/environments/staging-argocd-demo/params.libsonnet"
],
"removed": [
],
"modified": [
"ksapps/test-app/app.yaml"
]
},
"repository": {
"id": 123060978,
"name": "repo",
"full_name": "org/repo",
"owner": {
"name": "org",
"email": "org@users.noreply.github.com",
"login": "org",
"id": 12677113,
"avatar_url": "https://avatars0.githubusercontent.com/u/12677113?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/org",
"html_url": "https://github.com/org",
"followers_url": "https://api.github.com/users/org/followers",
"following_url": "https://api.github.com/users/org/following{/other_user}",
"gists_url": "https://api.github.com/users/org/gists{/gist_id}",
"starred_url": "https://api.github.com/users/org/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/org/subscriptions",
"organizations_url": "https://api.github.com/users/org/orgs",
"repos_url": "https://api.github.com/users/org/repos",
"events_url": "https://api.github.com/users/org/events{/privacy}",
"received_events_url": "https://api.github.com/users/org/received_events",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/org/repo",
"description": "Test Repository",
"fork": false,
"url": "https://github.com/org/repo",
"forks_url": "https://api.github.com/repos/org/repo/forks",
"keys_url": "https://api.github.com/repos/org/repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/org/repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/org/repo/teams",
"hooks_url": "https://api.github.com/repos/org/repo/hooks",
"issue_events_url": "https://api.github.com/repos/org/repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/org/repo/events",
"assignees_url": "https://api.github.com/repos/org/repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/org/repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/org/repo/tags",
"blobs_url": "https://api.github.com/repos/org/repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/org/repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/org/repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/org/repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/org/repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/org/repo/languages",
"stargazers_url": "https://api.github.com/repos/org/repo/stargazers",
"contributors_url": "https://api.github.com/repos/org/repo/contributors",
"subscribers_url": "https://api.github.com/repos/org/repo/subscribers",
"subscription_url": "https://api.github.com/repos/org/repo/subscription",
"commits_url": "https://api.github.com/repos/org/repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/org/repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/org/repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/org/repo/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/org/repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/org/repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/org/repo/merges",
"archive_url": "https://api.github.com/repos/org/repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/org/repo/downloads",
"issues_url": "https://api.github.com/repos/org/repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/org/repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/org/repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/org/repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/org/repo/labels{/name}",
"releases_url": "https://api.github.com/repos/org/repo/releases{/id}",
"deployments_url": "https://api.github.com/repos/org/repo/deployments",
"created_at": 1519698615,
"updated_at": "2018-05-04T22:37:55Z",
"pushed_at": 1525473610,
"git_url": "git://github.com/org/repo.git",
"ssh_url": "git@github.com:org/repo.git",
"clone_url": "https://github.com/org/repo.git",
"svn_url": "https://github.com/org/repo",
"homepage": null,
"size": 538,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 1,
"mirror_url": null,
"archived": false,
"open_issues_count": 0,
"license": null,
"forks": 1,
"open_issues": 0,
"watchers": 0,
"default_branch": "master",
"stargazers": 0,
"master_branch": "master"
},
"pusher": {
"name": "org",
"email": "org@users.noreply.github.com"
},
"sender": {
"login": "org",
"id": 12677113,
"avatar_url": "https://avatars0.githubusercontent.com/u/12677113?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/org",
"html_url": "https://github.com/org",
"followers_url": "https://api.github.com/users/org/followers",
"following_url": "https://api.github.com/users/org/following{/other_user}",
"gists_url": "https://api.github.com/users/org/gists{/gist_id}",
"starred_url": "https://api.github.com/users/org/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/org/subscriptions",
"organizations_url": "https://api.github.com/users/org/orgs",
"repos_url": "https://api.github.com/users/org/repos",
"events_url": "https://api.github.com/users/org/events{/privacy}",
"received_events_url": "https://api.github.com/users/org/received_events",
"type": "User",
"site_admin": false
}
}

View file

@ -0,0 +1,454 @@
{
"action": "assigned",
"number": 2,
"pull_request": {
"url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2",
"id": 279147437,
"node_id": "MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3",
"html_url": "https://github.com/Codertocat/Hello-World/pull/2",
"diff_url": "https://github.com/Codertocat/Hello-World/pull/2.diff",
"patch_url": "https://github.com/Codertocat/Hello-World/pull/2.patch",
"issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2",
"number": 2,
"state": "open",
"locked": false,
"title": "Update the README with new information.",
"user": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"body": "This is a pretty simple change that we need to pull into master.",
"created_at": "2019-05-15T15:20:33Z",
"updated_at": "2019-05-15T15:20:33Z",
"closed_at": null,
"merged_at": null,
"merge_commit_sha": null,
"assignee": null,
"assignees": [],
"requested_reviewers": [],
"requested_teams": [],
"labels": [],
"milestone": null,
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits",
"review_comments_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments",
"review_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821",
"head": {
"label": "Codertocat:changes",
"ref": "changes",
"sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
"user": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": "2019-05-15T15:19:25Z",
"updated_at": "2019-05-15T15:19:27Z",
"pushed_at": "2019-05-15T15:20:32Z",
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master",
"allow_squash_merge": true,
"allow_merge_commit": true,
"allow_rebase_merge": true,
"delete_branch_on_merge": false
}
},
"base": {
"label": "Codertocat:master",
"ref": "master",
"sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
"user": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": "2019-05-15T15:19:25Z",
"updated_at": "2019-05-15T15:19:27Z",
"pushed_at": "2019-05-15T15:20:32Z",
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master",
"allow_squash_merge": true,
"allow_merge_commit": true,
"allow_rebase_merge": true,
"delete_branch_on_merge": false
}
},
"_links": {
"self": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2"
},
"html": {
"href": "https://github.com/Codertocat/Hello-World/pull/2"
},
"issue": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2"
},
"comments": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments"
},
"review_comments": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments"
},
"review_comment": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}"
},
"commits": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits"
},
"statuses": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821"
}
},
"author_association": "OWNER",
"draft": false,
"merged": false,
"mergeable": null,
"rebaseable": null,
"mergeable_state": "unknown",
"merged_by": null,
"comments": 0,
"review_comments": 0,
"maintainer_can_modify": false,
"commits": 1,
"additions": 1,
"deletions": 1,
"changed_files": 1
},
"repository": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": "2019-05-15T15:19:25Z",
"updated_at": "2019-05-15T15:19:27Z",
"pushed_at": "2019-05-15T15:20:32Z",
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master"
},
"sender": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
}
}

View file

@ -0,0 +1,454 @@
{
"action": "opened",
"number": 2,
"pull_request": {
"url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2",
"id": 279147437,
"node_id": "MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3",
"html_url": "https://github.com/Codertocat/Hello-World/pull/2",
"diff_url": "https://github.com/Codertocat/Hello-World/pull/2.diff",
"patch_url": "https://github.com/Codertocat/Hello-World/pull/2.patch",
"issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2",
"number": 2,
"state": "open",
"locked": false,
"title": "Update the README with new information.",
"user": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"body": "This is a pretty simple change that we need to pull into master.",
"created_at": "2019-05-15T15:20:33Z",
"updated_at": "2019-05-15T15:20:33Z",
"closed_at": null,
"merged_at": null,
"merge_commit_sha": null,
"assignee": null,
"assignees": [],
"requested_reviewers": [],
"requested_teams": [],
"labels": [],
"milestone": null,
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits",
"review_comments_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments",
"review_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821",
"head": {
"label": "Codertocat:changes",
"ref": "changes",
"sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
"user": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": "2019-05-15T15:19:25Z",
"updated_at": "2019-05-15T15:19:27Z",
"pushed_at": "2019-05-15T15:20:32Z",
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master",
"allow_squash_merge": true,
"allow_merge_commit": true,
"allow_rebase_merge": true,
"delete_branch_on_merge": false
}
},
"base": {
"label": "Codertocat:master",
"ref": "master",
"sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
"user": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": "2019-05-15T15:19:25Z",
"updated_at": "2019-05-15T15:19:27Z",
"pushed_at": "2019-05-15T15:20:32Z",
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master",
"allow_squash_merge": true,
"allow_merge_commit": true,
"allow_rebase_merge": true,
"delete_branch_on_merge": false
}
},
"_links": {
"self": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2"
},
"html": {
"href": "https://github.com/Codertocat/Hello-World/pull/2"
},
"issue": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2"
},
"comments": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments"
},
"review_comments": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments"
},
"review_comment": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}"
},
"commits": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits"
},
"statuses": {
"href": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821"
}
},
"author_association": "OWNER",
"draft": false,
"merged": false,
"mergeable": null,
"rebaseable": null,
"mergeable_state": "unknown",
"merged_by": null,
"comments": 0,
"review_comments": 0,
"maintainer_can_modify": false,
"commits": 1,
"additions": 1,
"deletions": 1,
"changed_files": 1
},
"repository": {
"id": 186853002,
"node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
"name": "Hello-World",
"full_name": "Codertocat/Hello-World",
"private": false,
"owner": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
},
"html_url": "https://github.com/Codertocat/Hello-World",
"description": null,
"fork": false,
"url": "https://api.github.com/repos/Codertocat/Hello-World",
"forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
"keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
"hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
"issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
"events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
"assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
"branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
"tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
"blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
"languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
"stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
"contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
"subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
"subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
"commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
"compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
"archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
"issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
"pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
"milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
"notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
"releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
"deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
"created_at": "2019-05-15T15:19:25Z",
"updated_at": "2019-05-15T15:19:27Z",
"pushed_at": "2019-05-15T15:20:32Z",
"git_url": "git://github.com/Codertocat/Hello-World.git",
"ssh_url": "git@github.com:Codertocat/Hello-World.git",
"clone_url": "https://github.com/Codertocat/Hello-World.git",
"svn_url": "https://github.com/Codertocat/Hello-World",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_projects": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 2,
"license": null,
"forks": 0,
"open_issues": 2,
"watchers": 0,
"default_branch": "master"
},
"sender": {
"login": "Codertocat",
"id": 21031067,
"node_id": "MDQ6VXNlcjIxMDMxMDY3",
"avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/Codertocat",
"html_url": "https://github.com/Codertocat",
"followers_url": "https://api.github.com/users/Codertocat/followers",
"following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
"gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
"organizations_url": "https://api.github.com/users/Codertocat/orgs",
"repos_url": "https://api.github.com/users/Codertocat/repos",
"events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/Codertocat/received_events",
"type": "User",
"site_admin": false
}
}

View file

@ -0,0 +1,65 @@
{
"object_kind": "push",
"event_name": "push",
"before": "e5ba5f6c13b64670048daa88e4c053d60b0e115a",
"after": "bb0748feaa336d841c251017e4e374c22d0c8a98",
"ref": "refs/heads/master",
"checkout_sha": "bb0748feaa336d841c251017e4e374c22d0c8a98",
"message": null,
"user_id": 1,
"user_name": "name",
"user_username": "username",
"user_email": "",
"user_avatar": "",
"project_id": 1,
"project": {
"id": 1,
"name": "project",
"description": "",
"web_url": "https://gitlab/group/name",
"avatar_url": null,
"git_ssh_url": "ssh://git@gitlab:2222/group/name.git",
"git_http_url": "https://gitlab/group/name.git",
"namespace": "group",
"visibility_level": 1,
"path_with_namespace": "group/name",
"default_branch": "master",
"ci_config_path": null,
"homepage": "https://gitlab/group/name",
"url": "ssh://git@gitlab:2222/group/name.git",
"ssh_url": "ssh://git@gitlab:2222/group/name.git",
"http_url": "https://gitlab/group/name.git"
},
"commits": [
{
"id": "bb0748feaa336d841c251017e4e374c22d0c8a98",
"message": "Test commit message\n",
"timestamp": "2020-01-06T03:47:55Z",
"url": "https://gitlab/group/name/commit/bb0748feaa336d841c251017e4e374c22d0c8a98",
"author": {
"name": "User",
"email": "user@example.com"
},
"added": [
"file.yaml"
],
"modified": [
],
"removed": [
]
}
],
"total_commits_count": 1,
"push_options": {
},
"repository": {
"name": "name",
"url": "ssh://git@gitlab:2222/group/name.git",
"description": "",
"homepage": "https://gitlab/group/name",
"git_http_url": "https://gitlab/group/name.git",
"git_ssh_url": "ssh://git@gitlab:2222/group/name.git",
"visibility_level": 10
}
}

View file

@ -0,0 +1,3 @@
{
"event":"invalid"
}

View file

@ -0,0 +1,179 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/valyala/fasttemplate"
argoappsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
argoappsetv1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
type Renderer interface {
RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsetv1.ApplicationSetSyncPolicy, params map[string]string) (*argoappsv1.Application, error)
}
type Render struct {
}
func (r *Render) RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsetv1.ApplicationSetSyncPolicy, params map[string]string) (*argoappsv1.Application, error) {
if tmpl == nil {
return nil, fmt.Errorf("application template is empty ")
}
if len(params) == 0 {
return tmpl, nil
}
tmplBytes, err := json.Marshal(tmpl)
if err != nil {
return nil, err
}
fstTmpl := fasttemplate.New(string(tmplBytes), "{{", "}}")
replacedTmplStr, err := r.replace(fstTmpl, params, true)
if err != nil {
return nil, err
}
var replacedTmpl argoappsv1.Application
err = json.Unmarshal([]byte(replacedTmplStr), &replacedTmpl)
if err != nil {
return nil, err
}
// Add the 'resources-finalizer' finalizer if:
// The template application doesn't have any finalizers, and:
// a) there is no syncPolicy, or
// b) there IS a syncPolicy, but preserveResourcesOnDeletion is set to false
// See TestRenderTemplateParamsFinalizers in util_test.go for test-based definition of behaviour
if (syncPolicy == nil || !syncPolicy.PreserveResourcesOnDeletion) &&
(replacedTmpl.ObjectMeta.Finalizers == nil || len(replacedTmpl.ObjectMeta.Finalizers) == 0) {
replacedTmpl.ObjectMeta.Finalizers = []string{"resources-finalizer.argocd.argoproj.io"}
}
return &replacedTmpl, nil
}
// Replace executes basic string substitution of a template with replacement values.
// 'allowUnresolved' indicates whether or not it is acceptable to have unresolved variables
// remaining in the substituted template.
func (r *Render) replace(fstTmpl *fasttemplate.Template, replaceMap map[string]string, allowUnresolved bool) (string, error) {
var unresolvedErr error
replacedTmpl := fstTmpl.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
trimmedTag := strings.TrimSpace(tag)
replacement, ok := replaceMap[trimmedTag]
if len(trimmedTag) == 0 || !ok {
if allowUnresolved {
// just write the same string back
return w.Write([]byte(fmt.Sprintf("{{%s}}", tag)))
}
unresolvedErr = fmt.Errorf("failed to resolve {{%s}}", tag)
return 0, nil
}
// The following escapes any special characters (e.g. newlines, tabs, etc...)
// in preparation for substitution
replacement = strconv.Quote(replacement)
replacement = replacement[1 : len(replacement)-1]
return w.Write([]byte(replacement))
})
if unresolvedErr != nil {
return "", unresolvedErr
}
return replacedTmpl, nil
}
// Log a warning if there are unrecognized generators
func CheckInvalidGenerators(applicationSetInfo *argoappsetv1.ApplicationSet) {
hasInvalidGenerators, invalidGenerators := invalidGenerators(applicationSetInfo)
if len(invalidGenerators) > 0 {
gnames := []string{}
for n := range invalidGenerators {
gnames = append(gnames, n)
}
sort.Strings(gnames)
aname := applicationSetInfo.ObjectMeta.Name
msg := "ApplicationSet %s contains unrecognized generators: %s"
log.Warnf(msg, aname, strings.Join(gnames, ", "))
} else if hasInvalidGenerators {
name := applicationSetInfo.ObjectMeta.Name
msg := "ApplicationSet %s contains unrecognized generators"
log.Warnf(msg, name)
}
}
// Return true if there are unknown generators specified in the application set. If we can discover the names
// of these generators, return the names as the keys in a map
func invalidGenerators(applicationSetInfo *argoappsetv1.ApplicationSet) (bool, map[string]bool) {
names := make(map[string]bool)
hasInvalidGenerators := false
for index, generator := range applicationSetInfo.Spec.Generators {
v := reflect.Indirect(reflect.ValueOf(generator))
found := false
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.CanInterface() {
continue
}
if !reflect.ValueOf(field.Interface()).IsNil() {
found = true
break
}
}
if !found {
hasInvalidGenerators = true
addInvalidGeneratorNames(names, applicationSetInfo, index)
}
}
return hasInvalidGenerators, names
}
func addInvalidGeneratorNames(names map[string]bool, applicationSetInfo *argoappsetv1.ApplicationSet, index int) {
// The generator names are stored in the "kubectl.kubernetes.io/last-applied-configuration" annotation
config := applicationSetInfo.ObjectMeta.Annotations["kubectl.kubernetes.io/last-applied-configuration"]
var values map[string]interface{}
err := json.Unmarshal([]byte(config), &values)
if err != nil {
log.Warnf("couldn't unmarshal kubectl.kubernetes.io/last-applied-configuration: %+v", config)
return
}
spec, ok := values["spec"].(map[string]interface{})
if !ok {
log.Warn("coundn't get spec from kubectl.kubernetes.io/last-applied-configuration annotation")
return
}
generators, ok := spec["generators"].([]interface{})
if !ok {
log.Warn("coundn't get generators from kubectl.kubernetes.io/last-applied-configuration annotation")
return
}
if index >= len(generators) {
log.Warnf("index %d out of range %d for generator in kubectl.kubernetes.io/last-applied-configuration", index, len(generators))
return
}
generator, ok := generators[index].(map[string]interface{})
if !ok {
log.Warn("coundn't get generator from kubectl.kubernetes.io/last-applied-configuration annotation")
return
}
for key := range generator {
names[key] = true
break
}
}

View file

@ -0,0 +1,652 @@
package utils
import (
"testing"
"github.com/sirupsen/logrus"
logtest "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
argoappsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
argoappsetv1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
)
func TestRenderTemplateParams(t *testing.T) {
// Believe it or not, this is actually less complex than the equivalent solution using reflection
fieldMap := map[string]func(app *argoappsv1.Application) *string{}
fieldMap["Path"] = func(app *argoappsv1.Application) *string { return &app.Spec.Source.Path }
fieldMap["RepoURL"] = func(app *argoappsv1.Application) *string { return &app.Spec.Source.RepoURL }
fieldMap["TargetRevision"] = func(app *argoappsv1.Application) *string { return &app.Spec.Source.TargetRevision }
fieldMap["Chart"] = func(app *argoappsv1.Application) *string { return &app.Spec.Source.Chart }
fieldMap["Server"] = func(app *argoappsv1.Application) *string { return &app.Spec.Destination.Server }
fieldMap["Namespace"] = func(app *argoappsv1.Application) *string { return &app.Spec.Destination.Namespace }
fieldMap["Name"] = func(app *argoappsv1.Application) *string { return &app.Spec.Destination.Name }
fieldMap["Project"] = func(app *argoappsv1.Application) *string { return &app.Spec.Project }
emptyApplication := &argoappsv1.Application{
Spec: argoappsv1.ApplicationSpec{
Source: argoappsv1.ApplicationSource{
Path: "",
RepoURL: "",
TargetRevision: "",
Chart: "",
},
Destination: argoappsv1.ApplicationDestination{
Server: "",
Namespace: "",
Name: "",
},
Project: "",
},
}
tests := []struct {
name string
fieldVal string
params map[string]string
expectedVal string
}{
{
name: "simple substitution",
fieldVal: "{{one}}",
expectedVal: "two",
params: map[string]string{
"one": "two",
},
},
{
name: "simple substitution with whitespace",
fieldVal: "{{ one }}",
expectedVal: "two",
params: map[string]string{
"one": "two",
},
},
{
name: "template characters but not in a template",
fieldVal: "}} {{",
expectedVal: "}} {{",
params: map[string]string{
"one": "two",
},
},
{
name: "nested template",
fieldVal: "{{ }}",
expectedVal: "{{ }}",
params: map[string]string{
"one": "{{ }}",
},
},
{
name: "field with whitespace",
fieldVal: "{{ }}",
expectedVal: "{{ }}",
params: map[string]string{
" ": "two",
"": "three",
},
},
{
name: "template contains itself, containing itself",
fieldVal: "{{one}}",
expectedVal: "{{one}}",
params: map[string]string{
"{{one}}": "{{one}}",
},
},
{
name: "template contains itself, containing something else",
fieldVal: "{{one}}",
expectedVal: "{{one}}",
params: map[string]string{
"{{one}}": "{{two}}",
},
},
{
name: "templates are case sensitive",
fieldVal: "{{ONE}}",
expectedVal: "{{ONE}}",
params: map[string]string{
"{{one}}": "two",
},
},
{
name: "multiple on a line",
fieldVal: "{{one}}{{one}}",
expectedVal: "twotwo",
params: map[string]string{
"one": "two",
},
},
{
name: "multiple different on a line",
fieldVal: "{{one}}{{three}}",
expectedVal: "twofour",
params: map[string]string{
"one": "two",
"three": "four",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for fieldName, getPtrFunc := range fieldMap {
// Clone the template application
application := emptyApplication.DeepCopy()
// Set the value of the target field, to the test value
*getPtrFunc(application) = test.fieldVal
// Render the cloned application, into a new application
render := Render{}
newApplication, err := render.RenderTemplateParams(application, nil, test.params)
// Retrieve the value of the target field from the newApplication, then verify that
// the target field has been templated into the expected value
actualValue := *getPtrFunc(newApplication)
assert.Equal(t, test.expectedVal, actualValue, "Field '%s' had an unexpected value. expected: '%s' value: '%s'", fieldName, test.expectedVal, actualValue)
assert.NoError(t, err)
}
})
}
}
func TestRenderTemplateParamsFinalizers(t *testing.T) {
emptyApplication := &argoappsv1.Application{
Spec: argoappsv1.ApplicationSpec{
Source: argoappsv1.ApplicationSource{
Path: "",
RepoURL: "",
TargetRevision: "",
Chart: "",
},
Destination: argoappsv1.ApplicationDestination{
Server: "",
Namespace: "",
Name: "",
},
Project: "",
},
}
for _, c := range []struct {
testName string
syncPolicy *argoappsetv1.ApplicationSetSyncPolicy
existingFinalizers []string
expectedFinalizers []string
}{
{
testName: "existing finalizer should be preserved",
existingFinalizers: []string{"existing-finalizer"},
syncPolicy: nil,
expectedFinalizers: []string{"existing-finalizer"},
},
{
testName: "background finalizer should be preserved",
existingFinalizers: []string{"resources-finalizer.argocd.argoproj.io/background"},
syncPolicy: nil,
expectedFinalizers: []string{"resources-finalizer.argocd.argoproj.io/background"},
},
{
testName: "empty finalizer and empty sync should use standard finalizer",
existingFinalizers: nil,
syncPolicy: nil,
expectedFinalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
{
testName: "standard finalizer should be preserved",
existingFinalizers: []string{"resources-finalizer.argocd.argoproj.io"},
syncPolicy: nil,
expectedFinalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
{
testName: "empty array finalizers should use standard finalizer",
existingFinalizers: []string{},
syncPolicy: nil,
expectedFinalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
{
testName: "non-nil sync policy should use standard finalizer",
existingFinalizers: nil,
syncPolicy: &argoappsetv1.ApplicationSetSyncPolicy{},
expectedFinalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
{
testName: "preserveResourcesOnDeletion should not have a finalizer",
existingFinalizers: nil,
syncPolicy: &argoappsetv1.ApplicationSetSyncPolicy{
PreserveResourcesOnDeletion: true,
},
expectedFinalizers: nil,
},
{
testName: "user-specified finalizer should overwrite preserveResourcesOnDeletion",
existingFinalizers: []string{"resources-finalizer.argocd.argoproj.io/background"},
syncPolicy: &argoappsetv1.ApplicationSetSyncPolicy{
PreserveResourcesOnDeletion: true,
},
expectedFinalizers: []string{"resources-finalizer.argocd.argoproj.io/background"},
},
} {
t.Run(c.testName, func(t *testing.T) {
// Clone the template application
application := emptyApplication.DeepCopy()
application.Finalizers = c.existingFinalizers
params := map[string]string{
"one": "two",
}
// Render the cloned application, into a new application
render := Render{}
res, err := render.RenderTemplateParams(application, c.syncPolicy, params)
assert.Nil(t, err)
assert.ElementsMatch(t, res.Finalizers, c.expectedFinalizers)
})
}
}
func TestCheckInvalidGenerators(t *testing.T) {
scheme := runtime.NewScheme()
err := argoappsetv1.AddToScheme(scheme)
assert.Nil(t, err)
err = argoappsv1.AddToScheme(scheme)
assert.Nil(t, err)
for _, c := range []struct {
testName string
appSet argoappsetv1.ApplicationSet
expectedMsg string
}{
{
testName: "invalid generator, without annotation",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app-set",
Namespace: "namespace",
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: &argoappsetv1.ListGenerator{},
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: &argoappsetv1.GitGenerator{},
},
},
},
},
expectedMsg: "ApplicationSet test-app-set contains unrecognized generators",
},
{
testName: "invalid generator, with annotation",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app-set",
Namespace: "namespace",
Annotations: map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{
"spec":{
"generators":[
{"list":{}},
{"bbb":{}},
{"git":{}},
{"aaa":{}}
]
}
}`,
},
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: &argoappsetv1.ListGenerator{},
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: &argoappsetv1.GitGenerator{},
},
{
List: nil,
Clusters: nil,
Git: nil,
},
},
},
},
expectedMsg: "ApplicationSet test-app-set contains unrecognized generators: aaa, bbb",
},
} {
oldhooks := logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{})
defer logrus.StandardLogger().ReplaceHooks(oldhooks)
hook := logtest.NewGlobal()
CheckInvalidGenerators(&c.appSet)
assert.True(t, len(hook.Entries) >= 1, c.testName)
assert.NotNil(t, hook.LastEntry(), c.testName)
if hook.LastEntry() != nil {
assert.Equal(t, logrus.WarnLevel, hook.LastEntry().Level, c.testName)
assert.Equal(t, c.expectedMsg, hook.LastEntry().Message, c.testName)
}
hook.Reset()
}
}
func TestInvalidGenerators(t *testing.T) {
scheme := runtime.NewScheme()
err := argoappsetv1.AddToScheme(scheme)
assert.Nil(t, err)
err = argoappsv1.AddToScheme(scheme)
assert.Nil(t, err)
for _, c := range []struct {
testName string
appSet argoappsetv1.ApplicationSet
expectedInvalid bool
expectedNames map[string]bool
}{
{
testName: "valid generators, with annotation",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Annotations: map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{
"spec":{
"generators":[
{"list":{}},
{"cluster":{}},
{"git":{}}
]
}
}`,
},
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: &argoappsetv1.ListGenerator{},
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: &argoappsetv1.ClusterGenerator{},
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: &argoappsetv1.GitGenerator{},
},
},
},
},
expectedInvalid: false,
expectedNames: map[string]bool{},
},
{
testName: "invalid generators, no annotation",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: nil,
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: nil,
},
},
},
},
expectedInvalid: true,
expectedNames: map[string]bool{},
},
{
testName: "valid and invalid generators, no annotation",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: nil,
Clusters: &argoappsetv1.ClusterGenerator{},
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: &argoappsetv1.GitGenerator{},
},
},
},
},
expectedInvalid: true,
expectedNames: map[string]bool{},
},
{
testName: "valid and invalid generators, with annotation",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Annotations: map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{
"spec":{
"generators":[
{"cluster":{}},
{"bbb":{}},
{"git":{}},
{"aaa":{}}
]
}
}`,
},
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: nil,
Clusters: &argoappsetv1.ClusterGenerator{},
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: nil,
},
{
List: nil,
Clusters: nil,
Git: &argoappsetv1.GitGenerator{},
},
{
List: nil,
Clusters: nil,
Git: nil,
},
},
},
},
expectedInvalid: true,
expectedNames: map[string]bool{
"aaa": true,
"bbb": true,
},
},
{
testName: "invalid generator, annotation with missing spec",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Annotations: map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{
}`,
},
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: nil,
Clusters: nil,
Git: nil,
},
},
},
},
expectedInvalid: true,
expectedNames: map[string]bool{},
},
{
testName: "invalid generator, annotation with missing generators array",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Annotations: map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{
"spec":{
}
}`,
},
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: nil,
Clusters: nil,
Git: nil,
},
},
},
},
expectedInvalid: true,
expectedNames: map[string]bool{},
},
{
testName: "invalid generator, annotation with empty generators array",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Annotations: map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{
"spec":{
"generators":[
]
}
}`,
},
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: nil,
Clusters: nil,
Git: nil,
},
},
},
},
expectedInvalid: true,
expectedNames: map[string]bool{},
},
{
testName: "invalid generator, annotation with empty generator",
appSet: argoappsetv1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Annotations: map[string]string{
"kubectl.kubernetes.io/last-applied-configuration": `{
"spec":{
"generators":[
{}
]
}
}`,
},
},
Spec: argoappsetv1.ApplicationSetSpec{
Generators: []argoappsetv1.ApplicationSetGenerator{
{
List: nil,
Clusters: nil,
Git: nil,
},
},
},
},
expectedInvalid: true,
expectedNames: map[string]bool{},
},
} {
hasInvalid, names := invalidGenerators(&c.appSet)
assert.Equal(t, c.expectedInvalid, hasInvalid, c.testName)
assert.Equal(t, c.expectedNames, names, c.testName)
}
}

View file

@ -0,0 +1,299 @@
package utils
import (
"context"
"fmt"
"html"
"net/http"
"net/url"
"regexp"
"strings"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
argosettings "github.com/argoproj/argo-cd/v2/util/settings"
log "github.com/sirupsen/logrus"
"gopkg.in/go-playground/webhooks.v5/github"
"gopkg.in/go-playground/webhooks.v5/gitlab"
)
type WebhookHandler struct {
namespace string
github *github.Webhook
gitlab *gitlab.Webhook
client client.Client
}
type gitGeneratorInfo struct {
Revision string
TouchedHead bool
RepoRegexp *regexp.Regexp
}
type prGeneratorInfo struct {
Github *prGeneratorGithubInfo
}
type prGeneratorGithubInfo struct {
Repo string
Owner string
APIRegexp *regexp.Regexp
}
func NewWebhookHandler(namespace string, argocdSettingsMgr *argosettings.SettingsManager, client client.Client) (*WebhookHandler, error) {
// register the webhook secrets stored under "argocd-secret" for verifying incoming payloads
argocdSettings, err := argocdSettingsMgr.GetSettings()
if err != nil {
return nil, fmt.Errorf("Failed to get argocd settings: %v", err)
}
githubHandler, err := github.New(github.Options.Secret(argocdSettings.WebhookGitHubSecret))
if err != nil {
return nil, fmt.Errorf("Unable to init GitHub webhook: %v", err)
}
gitlabHandler, err := gitlab.New(gitlab.Options.Secret(argocdSettings.WebhookGitLabSecret))
if err != nil {
return nil, fmt.Errorf("Unable to init GitLab webhook: %v", err)
}
return &WebhookHandler{
namespace: namespace,
github: githubHandler,
gitlab: gitlabHandler,
client: client,
}, nil
}
func (h *WebhookHandler) HandleEvent(payload interface{}) {
gitGenInfo := getGitGeneratorInfo(payload)
prGenInfo := getPRGeneratorInfo(payload)
if gitGenInfo == nil && prGenInfo == nil {
return
}
appSetList := &v1alpha1.ApplicationSetList{}
err := h.client.List(context.Background(), appSetList, &client.ListOptions{})
if err != nil {
log.Errorf("Failed to list applicationsets: %v", err)
return
}
for _, appSet := range appSetList.Items {
shouldRefresh := false
for _, gen := range appSet.Spec.Generators {
// check if the ApplicationSet uses the git generator that is relevant to the payload
shouldRefresh = shouldRefreshGitGenerator(gen.Git, gitGenInfo) || shouldRefreshPRGenerator(gen.PullRequest, prGenInfo)
if shouldRefresh {
break
}
}
if shouldRefresh {
err := refreshApplicationSet(h.client, &appSet)
if err != nil {
log.Errorf("Failed to refresh ApplicationSet '%s' for controller reprocessing", appSet.Name)
continue
}
log.Infof("refresh ApplicationSet %v/%v from webhook", appSet.Namespace, appSet.Name)
}
}
}
func (h *WebhookHandler) Handler(w http.ResponseWriter, r *http.Request) {
var payload interface{}
var err error
switch {
case r.Header.Get("X-GitHub-Event") != "":
payload, err = h.github.Parse(r, github.PushEvent, github.PullRequestEvent)
case r.Header.Get("X-Gitlab-Event") != "":
payload, err = h.gitlab.Parse(r, gitlab.PushEvents, gitlab.TagEvents)
default:
log.Debug("Ignoring unknown webhook event")
http.Error(w, "Unknown webhook event", http.StatusBadRequest)
return
}
if err != nil {
log.Infof("Webhook processing failed: %s", err)
status := http.StatusBadRequest
if r.Method != "POST" {
status = http.StatusMethodNotAllowed
}
http.Error(w, fmt.Sprintf("Webhook processing failed: %s", html.EscapeString(err.Error())), status)
return
}
h.HandleEvent(payload)
}
func parseRevision(ref string) string {
refParts := strings.SplitN(ref, "/", 3)
return refParts[len(refParts)-1]
}
func getGitGeneratorInfo(payload interface{}) *gitGeneratorInfo {
var (
webURL string
revision string
touchedHead bool
)
switch payload := payload.(type) {
case github.PushPayload:
webURL = payload.Repository.HTMLURL
revision = parseRevision(payload.Ref)
touchedHead = payload.Repository.DefaultBranch == revision
case gitlab.PushEventPayload:
webURL = payload.Project.WebURL
revision = parseRevision(payload.Ref)
touchedHead = payload.Project.DefaultBranch == revision
default:
return nil
}
log.Infof("Received push event repo: %s, revision: %s, touchedHead: %v", webURL, revision, touchedHead)
urlObj, err := url.Parse(webURL)
if err != nil {
log.Errorf("Failed to parse repoURL '%s'", webURL)
return nil
}
regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?)` + urlObj.Hostname() + "(:[0-9]+|)[:/]" + urlObj.Path[1:] + "(\\.git)?"
repoRegexp, err := regexp.Compile(regexpStr)
if err != nil {
log.Errorf("Failed to compile regexp for repoURL '%s'", webURL)
return nil
}
return &gitGeneratorInfo{
RepoRegexp: repoRegexp,
TouchedHead: touchedHead,
}
}
func getPRGeneratorInfo(payload interface{}) *prGeneratorInfo {
var info prGeneratorInfo
switch payload := payload.(type) {
case github.PullRequestPayload:
if !isAllowedPullRequestAction(payload.Action) {
return nil
}
apiURL := payload.Repository.URL
urlObj, err := url.Parse(apiURL)
if err != nil {
log.Errorf("Failed to parse repoURL '%s'", apiURL)
return nil
}
regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?)` + urlObj.Hostname() + "(:[0-9]+|)[:/]"
apiRegexp, err := regexp.Compile(regexpStr)
if err != nil {
log.Errorf("Failed to compile regexp for repoURL '%s'", apiURL)
return nil
}
info.Github = &prGeneratorGithubInfo{
Repo: payload.Repository.Name,
Owner: payload.Repository.Owner.Login,
APIRegexp: apiRegexp,
}
default:
return nil
}
return &info
}
// allowedPullRequestActions is a list of actions that allow refresh
var allowedPullRequestActions = []string{
"opened",
"closed",
"synchronize",
"labeled",
"reopened",
"unlabeled",
}
func isAllowedPullRequestAction(action string) bool {
for _, allow := range allowedPullRequestActions {
if allow == action {
return true
}
}
return false
}
func shouldRefreshGitGenerator(gen *v1alpha1.GitGenerator, info *gitGeneratorInfo) bool {
if gen == nil || info == nil {
return false
}
if !gitGeneratorUsesURL(gen, info.Revision, info.RepoRegexp) {
return false
}
if !genRevisionHasChanged(gen, info.Revision, info.TouchedHead) {
return false
}
return true
}
func genRevisionHasChanged(gen *v1alpha1.GitGenerator, revision string, touchedHead bool) bool {
targetRev := parseRevision(gen.Revision)
if targetRev == "HEAD" || targetRev == "" { // revision is head
return touchedHead
}
return targetRev == revision
}
func gitGeneratorUsesURL(gen *v1alpha1.GitGenerator, webURL string, repoRegexp *regexp.Regexp) bool {
if !repoRegexp.MatchString(gen.RepoURL) {
log.Debugf("%s does not match %s", gen.RepoURL, repoRegexp.String())
return false
}
log.Debugf("%s uses repoURL %s", gen.RepoURL, webURL)
return true
}
func shouldRefreshPRGenerator(gen *v1alpha1.PullRequestGenerator, info *prGeneratorInfo) bool {
if gen == nil || info == nil {
return false
}
if gen.Github == nil || info.Github == nil {
return false
}
if gen.Github.Owner != info.Github.Owner {
return false
}
if gen.Github.Repo != info.Github.Repo {
return false
}
api := gen.Github.API
if api == "" {
api = "https://api.github.com/"
}
if !info.Github.APIRegexp.MatchString(api) {
log.Debugf("%s does not match %s", gen.Github.API, info.Github.APIRegexp.String())
return false
}
return true
}
func refreshApplicationSet(c client.Client, appSet *v1alpha1.ApplicationSet) error {
// patch the ApplicationSet with the refresh annotation to reconcile
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
err := c.Get(context.Background(), types.NamespacedName{Name: appSet.Name, Namespace: appSet.Namespace}, appSet)
if err != nil {
return err
}
if appSet.Annotations == nil {
appSet.Annotations = map[string]string{}
}
appSet.Annotations[common.AnnotationApplicationSetRefresh] = "true"
return c.Patch(context.Background(), appSet, client.Merge)
})
}

View file

@ -0,0 +1,208 @@
package utils
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/argoproj/argo-cd/v2/common"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
argosettings "github.com/argoproj/argo-cd/v2/util/settings"
)
func TestWebhookHandler(t *testing.T) {
tt := []struct {
desc string
headerKey string
headerValue string
effectedAppSets []string
payloadFile string
expectedStatusCode int
expectedRefresh bool
}{
{
desc: "WebHook from a GitHub repository via Commit",
headerKey: "X-GitHub-Event",
headerValue: "push",
payloadFile: "github-commit-event.json",
effectedAppSets: []string{"git-github"},
expectedStatusCode: http.StatusOK,
expectedRefresh: true,
},
{
desc: "WebHook from a GitLab repository via Commit",
headerKey: "X-Gitlab-Event",
headerValue: "Push Hook",
payloadFile: "gitlab-event.json",
effectedAppSets: []string{"git-gitlab"},
expectedStatusCode: http.StatusOK,
expectedRefresh: true,
},
{
desc: "WebHook with an unknown event",
headerKey: "X-Random-Event",
headerValue: "Push Hook",
payloadFile: "gitlab-event.json",
effectedAppSets: []string{"git-gitlab"},
expectedStatusCode: http.StatusBadRequest,
expectedRefresh: false,
},
{
desc: "WebHook with an invalid event",
headerKey: "X-Random-Event",
headerValue: "Push Hook",
payloadFile: "invalid-event.json",
effectedAppSets: []string{"git-gitlab"},
expectedStatusCode: http.StatusBadRequest,
expectedRefresh: false,
},
{
desc: "WebHook from a GitHub repository via pull_reqeuest opened event",
headerKey: "X-GitHub-Event",
headerValue: "pull_request",
payloadFile: "github-pull-request-opened-event.json",
effectedAppSets: []string{"pull-request-github"},
expectedStatusCode: http.StatusOK,
expectedRefresh: true,
},
{
desc: "WebHook from a GitHub repository via pull_reqeuest assigned event",
headerKey: "X-GitHub-Event",
headerValue: "pull_request",
payloadFile: "github-pull-request-assigned-event.json",
effectedAppSets: []string{"pull-request-github"},
expectedStatusCode: http.StatusOK,
expectedRefresh: false,
},
}
namespace := "test"
fakeClient := newFakeClient(namespace)
scheme := runtime.NewScheme()
err := argoprojiov1alpha1.AddToScheme(scheme)
assert.Nil(t, err)
err = argov1alpha1.AddToScheme(scheme)
assert.Nil(t, err)
for _, test := range tt {
t.Run(test.desc, func(t *testing.T) {
fc := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
fakeAppWithGitGenerator("git-github", namespace, "https://github.com/org/repo"),
fakeAppWithGitGenerator("git-gitlab", namespace, "https://gitlab/group/name"),
fakeAppWithPullRequestGenerator("pull-request-github", namespace, "Codertocat", "Hello-World"),
).Build()
set := argosettings.NewSettingsManager(context.TODO(), fakeClient, namespace)
h, err := NewWebhookHandler(namespace, set, fc)
assert.Nil(t, err)
req := httptest.NewRequest("POST", "/api/webhook", nil)
req.Header.Set(test.headerKey, test.headerValue)
eventJSON, err := ioutil.ReadFile(filepath.Join("testdata", test.payloadFile))
assert.NoError(t, err)
req.Body = ioutil.NopCloser(bytes.NewReader(eventJSON))
w := httptest.NewRecorder()
h.Handler(w, req)
assert.Equal(t, w.Code, test.expectedStatusCode)
list := &argoprojiov1alpha1.ApplicationSetList{}
err = fc.List(context.TODO(), list)
assert.Nil(t, err)
for i := range list.Items {
gotAppSet := &list.Items[i]
for _, appSetName := range test.effectedAppSets {
if appSetName == gotAppSet.Name {
if expected, got := test.expectedRefresh, gotAppSet.RefreshRequired(); expected != got {
t.Errorf("unexpected RefreshRequired() expect: %v got: %v", expected, got)
}
} else {
assert.False(t, gotAppSet.RefreshRequired())
}
}
}
})
}
}
func TestGenRevisionHasChanged(t *testing.T) {
assert.True(t, genRevisionHasChanged(&v1alpha1.GitGenerator{}, "master", true))
assert.False(t, genRevisionHasChanged(&v1alpha1.GitGenerator{}, "master", false))
assert.True(t, genRevisionHasChanged(&v1alpha1.GitGenerator{Revision: "dev"}, "dev", true))
assert.False(t, genRevisionHasChanged(&v1alpha1.GitGenerator{Revision: "dev"}, "master", false))
assert.True(t, genRevisionHasChanged(&v1alpha1.GitGenerator{Revision: "refs/heads/dev"}, "dev", true))
assert.False(t, genRevisionHasChanged(&v1alpha1.GitGenerator{Revision: "refs/heads/dev"}, "master", false))
}
func fakeAppWithGitGenerator(name, namespace, repo string) *argoprojiov1alpha1.ApplicationSet {
return &argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
Git: &argoprojiov1alpha1.GitGenerator{
RepoURL: repo,
},
},
},
},
}
}
func fakeAppWithPullRequestGenerator(name, namespace, owner, repo string) *argoprojiov1alpha1.ApplicationSet {
return &argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
{
PullRequest: &argoprojiov1alpha1.PullRequestGenerator{
Github: &argoprojiov1alpha1.PullRequestGeneratorGithub{
Owner: owner,
Repo: repo,
},
},
},
},
},
}
}
func newFakeClient(ns string) *kubefake.Clientset {
s := runtime.NewScheme()
s.AddKnownTypes(argoprojiov1alpha1.GroupVersion, &argoprojiov1alpha1.ApplicationSet{})
return kubefake.NewSimpleClientset(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "argocd-cm", Namespace: ns, Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
}}}, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: common.ArgoCDSecretName,
Namespace: ns,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string][]byte{
"server.secretkey": nil,
},
})
}

View file

@ -0,0 +1,218 @@
package command
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/argoproj/pkg/stats"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"github.com/argoproj/argo-cd/v2/applicationset/controllers"
"github.com/argoproj/argo-cd/v2/applicationset/generators"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/v2/applicationset/services"
appv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
appsetv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/v2/util/cli"
"github.com/argoproj/argo-cd/v2/util/db"
argosettings "github.com/argoproj/argo-cd/v2/util/settings"
)
func NewCommand() *cobra.Command {
var (
clientConfig clientcmd.ClientConfig
metricsAddr string
probeBindAddr string
webhookAddr string
enableLeaderElection bool
namespace string
argocdRepoServer string
policy string
debugLog bool
dryRun bool
logFormat string
logLevel string
)
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = appsetv1alpha1.AddToScheme(scheme)
_ = appv1alpha1.AddToScheme(scheme)
var command = cobra.Command{
Use: "controller",
Short: "Starts Argo CD ApplicationSet controller",
RunE: func(c *cobra.Command, args []string) error {
restConfig, err := clientConfig.ClientConfig()
if err != nil {
return err
}
vers := common.GetVersion()
restConfig.UserAgent = fmt.Sprintf("argocd-applicationset-controller/%s (%s)", vers.Version, vers.Platform)
if namespace == "" {
namespace, _, err = clientConfig.Namespace()
if err != nil {
return err
}
}
level, err := log.ParseLevel(logLevel)
if err != nil {
return err
}
log.SetLevel(level)
log.Info(fmt.Sprintf("ApplicationSet controller %s using namespace '%s' ", vers.Version, namespace), "namespace", namespace, "COMMIT_ID", vers.GitCommit)
switch strings.ToLower(logFormat) {
case "json":
log.SetFormatter(&log.JSONFormatter{})
case "text":
if os.Getenv("FORCE_LOG_COLORS") == "1" {
log.SetFormatter(&log.TextFormatter{ForceColors: true})
}
default:
return fmt.Errorf("Unknown log format '%s'", logFormat)
}
policyObj, exists := utils.Policies[policy]
if !exists {
log.Info("Policy value can be: sync, create-only, create-update")
os.Exit(1)
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
// Our cache and thus watches and client queries are restricted to the namespace we're running in. This assumes
// the applicationset controller is in the same namespace as argocd, which should be the same namespace of
// all cluster Secrets and Applications we interact with.
NewCache: cache.MultiNamespacedCacheBuilder([]string{namespace}),
HealthProbeBindAddress: probeBindAddr,
Port: 9443,
LeaderElection: enableLeaderElection,
LeaderElectionID: "58ac56fa.applicationsets.argoproj.io",
DryRunClient: dryRun,
})
if err != nil {
log.Error(err, "unable to start manager")
os.Exit(1)
}
dynamicClient, err := dynamic.NewForConfig(mgr.GetConfig())
if err != nil {
return err
}
k8sClient, err := kubernetes.NewForConfig(mgr.GetConfig())
if err != nil {
return err
}
argoSettingsMgr := argosettings.NewSettingsManager(context.Background(), k8sClient, namespace)
appSetConfig := appclientset.NewForConfigOrDie(mgr.GetConfig())
argoCDDB := db.NewDB(namespace, argoSettingsMgr, k8sClient)
// start a webhook server that listens to incoming webhook payloads
webhookHandler, err := utils.NewWebhookHandler(namespace, argoSettingsMgr, mgr.GetClient())
if err != nil {
log.Error(err, "failed to create webhook handler")
}
if webhookHandler != nil {
startWebhookServer(webhookHandler, webhookAddr)
}
askPassServer := askpass.NewServer()
terminalGenerators := map[string]generators.Generator{
"List": generators.NewListGenerator(),
"Clusters": generators.NewClusterGenerator(mgr.GetClient(), context.Background(), k8sClient, namespace),
"Git": generators.NewGitGenerator(services.NewArgoCDService(argoCDDB, askPassServer, argocdRepoServer)),
"SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient()),
"ClusterDecisionResource": generators.NewDuckTypeGenerator(context.Background(), dynamicClient, k8sClient, namespace),
"PullRequest": generators.NewPullRequestGenerator(mgr.GetClient()),
}
nestedGenerators := map[string]generators.Generator{
"List": terminalGenerators["List"],
"Clusters": terminalGenerators["Clusters"],
"Git": terminalGenerators["Git"],
"SCMProvider": terminalGenerators["SCMProvider"],
"ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"],
"PullRequest": terminalGenerators["PullRequest"],
"Matrix": generators.NewMatrixGenerator(terminalGenerators),
"Merge": generators.NewMergeGenerator(terminalGenerators),
}
topLevelGenerators := map[string]generators.Generator{
"List": terminalGenerators["List"],
"Clusters": terminalGenerators["Clusters"],
"Git": terminalGenerators["Git"],
"SCMProvider": terminalGenerators["SCMProvider"],
"ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"],
"PullRequest": terminalGenerators["PullRequest"],
"Matrix": generators.NewMatrixGenerator(nestedGenerators),
"Merge": generators.NewMergeGenerator(nestedGenerators),
}
if err = (&controllers.ApplicationSetReconciler{
Generators: topLevelGenerators,
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ApplicationSet"),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("applicationset-controller"),
Renderer: &utils.Render{},
Policy: policyObj,
ArgoAppClientset: appSetConfig,
KubeClientset: k8sClient,
ArgoDB: argoCDDB,
}).SetupWithManager(mgr); err != nil {
log.Error(err, "unable to create controller", "controller", "ApplicationSet")
os.Exit(1)
}
stats.StartStatsTicker(10 * time.Minute)
log.Info("Starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
log.Error(err, "problem running manager")
os.Exit(1)
}
return nil
},
}
clientConfig = cli.AddKubectlFlagsToCmd(&command)
command.Flags().StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
command.Flags().StringVar(&probeBindAddr, "probe-addr", ":8081", "The address the probe endpoint binds to.")
command.Flags().StringVar(&webhookAddr, "webhook-addr", ":7000", "The address the webhook endpoint binds to.")
command.Flags().BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
command.Flags().StringVar(&namespace, "namespace", "", "Argo CD repo namespace (default: argocd)")
command.Flags().StringVar(&argocdRepoServer, "argocd-repo-server", "argocd-repo-server:8081", "Argo CD repo server address")
command.Flags().StringVar(&policy, "policy", "sync", "Modify how application is synced between the generator and the cluster. Default is 'sync' (create & update & delete), options: 'create-only', 'create-update' (no deletion)")
command.Flags().BoolVar(&debugLog, "debug", false, "Print debug logs. Takes precedence over loglevel")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
command.Flags().BoolVar(&dryRun, "dry-run", false, "Enable dry run mode")
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
return &command
}
func startWebhookServer(webhookHandler *utils.WebhookHandler, webhookAddr string) {
mux := http.NewServeMux()
mux.HandleFunc("/api/webhook", webhookHandler.Handler)
go func() {
log.Info("Starting webhook server")
err := http.ListenAndServe(webhookAddr, mux)
if err != nil {
log.Error(err, "failed to start webhook server")
os.Exit(1)
}
}()
}

View file

@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"
appcontroller "github.com/argoproj/argo-cd/v2/cmd/argocd-application-controller/commands"
applicationset "github.com/argoproj/argo-cd/v2/cmd/argocd-applicationset-controller/commands"
cmpserver "github.com/argoproj/argo-cd/v2/cmd/argocd-cmp-server/commands"
dex "github.com/argoproj/argo-cd/v2/cmd/argocd-dex/commands"
gitaskpass "github.com/argoproj/argo-cd/v2/cmd/argocd-git-ask-pass/commands"
@ -44,6 +45,8 @@ func main() {
command = notification.NewCommand()
case "argocd-git-ask-pass":
command = gitaskpass.NewCommand()
case "argocd-applicationset-controller":
command = applicationset.NewCommand()
default:
command = cli.NewCommand()
}

View file

@ -275,3 +275,8 @@ func GetCMPWorkDir() string {
}
return filepath.Join(os.TempDir(), DefaultCMPWorkDirName)
}
const (
// AnnotationApplicationRefresh is an annotation that is added when an ApplicationSet is requested to be refreshed by a webhook. The ApplicationSet controller will remove this annotation at the end of reconcilation.
AnnotationApplicationSetRefresh = "argocd.argoproj.io/application-set-refresh"
)

10
go.mod
View file

@ -35,6 +35,7 @@ require (
github.com/golang/protobuf v1.5.2
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/go-cmp v0.5.6
github.com/google/go-github/v35 v35.3.0
github.com/google/go-jsonnet v0.18.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.1.2
@ -44,9 +45,12 @@ require (
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/grpc-ecosystem/grpc-gateway v1.16.0
github.com/hashicorp/go-retryablehttp v0.7.0
github.com/imdario/mergo v0.3.12
github.com/improbable-eng/grpc-web v0.0.0-20181111100011-16092bd1d58a
github.com/itchyny/gojq v0.12.3
github.com/jeremywohl/flatten v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/ktrysmt/go-bitbucket v0.9.40
github.com/malexdev/utfutil v0.0.0-20180510171754-00c8d4a8e7a8 // indirect
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-zglob v0.0.3
@ -65,7 +69,9 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/undefinedlabs/go-mpatch v1.0.6
github.com/valyala/fasttemplate v1.2.1
github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0
github.com/xanzy/go-gitlab v0.60.0
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/net v0.0.0-20211209124913-491a49abca63
@ -139,6 +145,7 @@ require (
github.com/go-stack/stack v1.8.0 // indirect
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.4.0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/go-github/v41 v41.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
@ -148,7 +155,6 @@ require (
github.com/gregdel/pushover v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/huandu/xstrings v1.3.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/itchyny/timefmt-go v0.1.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
@ -183,6 +189,7 @@ require (
github.com/sergi/go-diff v1.1.0 // indirect
github.com/slack-go/slack v0.10.1 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vmihailenco/go-tinylfu v0.2.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
@ -198,6 +205,7 @@ require (
golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gomodules.xyz/envconfig v1.3.1-0.20190308184047-426f31af0d45 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
gomodules.xyz/notify v0.1.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

23
go.sum
View file

@ -507,6 +507,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v35 v35.3.0 h1:fU+WBzuukn0VssbayTT+Zo3/ESKX9JYWjbZTLOTEyho=
github.com/google/go-github/v35 v35.3.0/go.mod h1:yWB7uCcVWaUbUP74Aq3whuMySRMatyRmq5U9FTNlbio=
github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg=
@ -598,6 +600,7 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
@ -650,6 +653,8 @@ github.com/itchyny/timefmt-go v0.1.2/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrn
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@ -680,6 +685,8 @@ github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0t
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
@ -706,6 +713,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ktrysmt/go-bitbucket v0.9.40 h1:LcvdyW7u58vfbUi9bCQB+ihyqDzoy+9WBq/odmBsXrg=
github.com/ktrysmt/go-bitbucket v0.9.40/go.mod h1:FWxy2UK7GlK5b0NSJGc5hPqnssVlkNnsChvyuOf/Xno=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
@ -784,6 +793,7 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
@ -1055,6 +1065,10 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
@ -1068,6 +1082,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0 h1:qqllXPzXh+So+mmANlX/gCJrgo+1kQyshMoQ+NASzm0=
github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4=
github.com/xanzy/go-gitlab v0.60.0 h1:HaIlc14k4t9eJjAhY0Gmq2fBHgKd1MthBn3+vzDtsbA=
github.com/xanzy/go-gitlab v0.60.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@ -1131,6 +1147,7 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
@ -1218,6 +1235,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1-0.20210830214625-1b1db11ec8f4 h1:7Qds88gNaRx0Dz/1wOwXlR7asekh1B1u26wEwN6FcEI=
golang.org/x/mod v0.5.1-0.20210830214625-1b1db11ec8f4/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1279,7 +1297,9 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1311,6 +1331,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1565,8 +1586,10 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=

13
hack/boilerplate.go.txt Normal file
View file

@ -0,0 +1,13 @@
/*
Licensed 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.
*/

View file

@ -99,30 +99,73 @@ func checkErr(err error) {
}
func main() {
crds := getCustomResourceDefinitions()
crdsapp := getCustomResourceDefinitions()
for kind, path := range kindToCRDPath {
crd := crds[kind]
crd := crdsapp[kind]
if crd == nil {
panic(fmt.Sprintf("CRD of kind %s was not generated", kind))
}
jsonBytes, err := json.Marshal(crd)
checkErr(err)
var r unstructured.Unstructured
err = json.Unmarshal(jsonBytes, &r.Object)
checkErr(err)
// clean up crd yaml before marshalling
unstructured.RemoveNestedField(r.Object, "status")
unstructured.RemoveNestedField(r.Object, "metadata", "creationTimestamp")
jsonBytes, err = json.MarshalIndent(r.Object, "", " ")
checkErr(err)
yamlBytes, err := yaml.JSONToYAML(jsonBytes)
checkErr(err)
err = ioutil.WriteFile(path, yamlBytes, 0644)
checkErr(err)
writeCRDintoFile(crd, path)
}
crdsappset := getCRDApplicationset()
crd := crdsappset[application.ApplicationSetFullName]
if crd == nil {
panic(fmt.Sprintf("CRD of kind %s was not generated", application.ApplicationSetFullName))
}
writeCRDintoFile(crd, "manifests/crds/applicationset-crd.yaml")
}
func writeCRDintoFile(crd *extensionsobj.CustomResourceDefinition, path string) {
jsonBytes, err := json.Marshal(crd)
checkErr(err)
var r unstructured.Unstructured
err = json.Unmarshal(jsonBytes, &r.Object)
checkErr(err)
// clean up crd yaml before marshalling
unstructured.RemoveNestedField(r.Object, "status")
unstructured.RemoveNestedField(r.Object, "metadata", "creationTimestamp")
jsonBytes, err = json.MarshalIndent(r.Object, "", " ")
checkErr(err)
yamlBytes, err := yaml.JSONToYAML(jsonBytes)
checkErr(err)
err = ioutil.WriteFile(path, yamlBytes, 0644)
checkErr(err)
}
// getCRDApplicationset ... generated Custom Resources nased on types defined in pkg/apis/applicationset
func getCRDApplicationset() map[string]*extensionsobj.CustomResourceDefinition {
crdYamlBytes, err := exec.Command(
"controller-gen",
"paths=./pkg/apis/applicationset/...",
"crd:crdVersions=v1,maxDescLen=0",
"output:crd:stdout",
).Output()
checkErr(err)
// clean up stuff left by controller-gen
deleteFile("config/argoproj.io_applicationsets.yaml")
deleteFile("config")
objs, err := kube.SplitYAML(crdYamlBytes)
checkErr(err)
crds := make(map[string]*extensionsobj.CustomResourceDefinition)
for i := range objs {
un := objs[i]
// We need to completely remove validation of problematic fields such as creationTimestamp,
// which get marshalled to `null`, but are typed as as a `string` during Open API validation
removeValidation(un, "metadata.creationTimestamp")
crd := toCRD(un)
crd.Labels = map[string]string{
"app.kubernetes.io/name": crd.Name,
}
delete(crd.Annotations, "controller-gen.kubebuilder.io/version")
crd.Spec.Scope = "Namespaced"
crds[crd.Name] = crd
}
return crds
}

View file

@ -27,35 +27,23 @@ if [ "$IMAGE_TAG" = "" ]; then
IMAGE_TAG=latest
fi
# bundle_with_addons bundles given kustomize base with either stable or latest version of addons
function bundle_with_addons() {
for addon in $(ls $SRCROOT/manifests/addons | grep -v README.md); do
ADDON_BASE="latest"
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ $branch = release-* ]]; then
ADDON_BASE="stable"
fi
rm -rf $SRCROOT/manifests/_tmp-bundle && mkdir -p $SRCROOT/manifests/_tmp-bundle
cat << EOF >> $SRCROOT/manifests/_tmp-bundle/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../$1
- ../addons/$addon/$ADDON_BASE
EOF
echo "${AUTOGENMSG}" > $2
$KUSTOMIZE build $SRCROOT/manifests/_tmp-bundle >> $2
done
}
$KUSTOMIZE version
cd ${SRCROOT}/manifests/base && $KUSTOMIZE edit set image quay.io/argoproj/argocd=${IMAGE_NAMESPACE}/argocd:${IMAGE_TAG}
cd ${SRCROOT}/manifests/ha/base && $KUSTOMIZE edit set image quay.io/argoproj/argocd=${IMAGE_NAMESPACE}/argocd:${IMAGE_TAG}
cd ${SRCROOT}/manifests/core-install && $KUSTOMIZE edit set image quay.io/argoproj/argocd=${IMAGE_NAMESPACE}/argocd:${IMAGE_TAG}
bundle_with_addons "cluster-install" "${SRCROOT}/manifests/install.yaml"
bundle_with_addons "namespace-install" "${SRCROOT}/manifests/namespace-install.yaml"
bundle_with_addons "ha/cluster-install" "${SRCROOT}/manifests/ha/install.yaml"
bundle_with_addons "ha/namespace-install" "${SRCROOT}/manifests/ha/namespace-install.yaml"
bundle_with_addons "core-install" "${SRCROOT}/manifests/core-install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/cluster-install" >> "${SRCROOT}/manifests/install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/namespace-install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/namespace-install" >> "${SRCROOT}/manifests/namespace-install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/ha/install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/ha/cluster-install" >> "${SRCROOT}/manifests/ha/install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/ha/namespace-install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/ha/namespace-install" >> "${SRCROOT}/manifests/ha/namespace-install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/core-install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/core-install" >> "${SRCROOT}/manifests/core-install.yaml"

View file

@ -9,6 +9,13 @@ PROJECT_ROOT=$(cd $(dirname "$0")/.. ; pwd)
PATH="${PROJECT_ROOT}/dist:${PATH}"
VERSION="v1alpha1"
[ -e ./v2 ] || ln -s . v2
controller-gen \
object:headerFile=${PROJECT_ROOT}/hack/boilerplate.go.txt \
paths=github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/${VERSION} \
$@
[ -e ./v2 ] && rm -rf v2
[ -e ./v2 ] || ln -s . v2
openapi-gen \
--go-header-file ${PROJECT_ROOT}/hack/custom-boilerplate.go.txt \
@ -18,6 +25,7 @@ openapi-gen \
$@
[ -e ./v2 ] && rm -rf v2
export GO111MODULE=off
export GO111MODULE=on
go build -o ./dist/gen-crd-spec ${PROJECT_ROOT}/hack/gen-crd-spec
./dist/gen-crd-spec

View file

@ -1,4 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://raw.githubusercontent.com/argoproj/applicationset/master/manifests/install.yaml

View file

@ -1,4 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://raw.githubusercontent.com/argoproj/applicationset/v0.4.1/manifests/install.yaml

View file

@ -0,0 +1,54 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: argocd-applicationset-controller
app.kubernetes.io/part-of: argocd-applicationset
app.kubernetes.io/component: controller
name: argocd-applicationset-controller
spec:
selector:
matchLabels:
app.kubernetes.io/name: argocd-applicationset-controller
template:
metadata:
labels:
app.kubernetes.io/name: argocd-applicationset-controller
spec:
containers:
- command:
- entrypoint.sh
- applicationset-controller
image: quay.io/argoproj/argocd:latest
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
- containerPort: 7000
name: webhook
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- mountPath: /app/config/ssh
name: ssh-known-hosts
- mountPath: /app/config/tls
name: tls-certs
- mountPath: /app/config/gpg/source
name: gpg-keys
- mountPath: /app/config/gpg/keys
name: gpg-keyring
serviceAccountName: argocd-applicationset-controller
volumes:
- configMap:
name: argocd-ssh-known-hosts-cm
name: ssh-known-hosts
- configMap:
name: argocd-tls-certs-cm
name: tls-certs
- configMap:
name: argocd-gpg-keys-cm
name: gpg-keys
- emptyDir: {}
name: gpg-keyring

View file

@ -0,0 +1,60 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
labels:
app.kubernetes.io/name: argocd-applicationset-controller
app.kubernetes.io/part-of: argocd-applicationset
app.kubernetes.io/component: controller
name: argocd-applicationset-controller
rules:
- apiGroups:
- argoproj.io
resources:
- applications
- appprojects
- applicationsets
- applicationsets/finalizers
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- argoproj.io
resources:
- applicationsets/status
verbs:
- get
- patch
- update
- apiGroups:
- ''
resources:
- events
verbs:
- create
- get
- list
- patch
- watch
- apiGroups:
- ''
resources:
- secrets
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- apps
- extensions
resources:
- deployments
verbs:
- get
- list
- watch

View file

@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
app.kubernetes.io/name: argocd-applicationset-controller
app.kubernetes.io/part-of: argocd-applicationset
app.kubernetes.io/component: controller
name: argocd-applicationset-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: argocd-applicationset-controller
subjects:
- kind: ServiceAccount
name: argocd-applicationset-controller

View file

@ -0,0 +1,9 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/name: argocd-applicationset-controller
app.kubernetes.io/part-of: argocd-applicationset
app.kubernetes.io/component: controller
name: argocd-applicationset-controller

View file

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/name: argocd-applicationset-controller
app.kubernetes.io/part-of: argocd-applicationset
name: argocd-applicationset-controller
spec:
ports:
- name: webhook
port: 7000
protocol: TCP
targetPort: webhook
selector:
app.kubernetes.io/name: argocd-applicationset-controller

View file

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- argocd-applicationset-controller-rolebinding.yaml
- argocd-applicationset-controller-sa.yaml
- argocd-applicationset-controller-deployment.yaml
- argocd-applicationset-controller-role.yaml
- argocd-applicationset-controller-service.yaml

View file

@ -14,3 +14,4 @@ resources:
- ./config
- ./redis
- ./notification
- ./applicationset-controller

View file

@ -2153,9 +2153,8 @@ spec:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.3.0
creationTimestamp: null
labels:
app.kubernetes.io/name: applicationsets.argoproj.io
name: applicationsets.argoproj.io
spec:
group: argoproj.io
@ -2379,25 +2378,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -2685,25 +2665,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -2993,25 +2954,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3277,25 +3219,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3591,25 +3514,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3897,25 +3801,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4205,25 +4090,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4489,25 +4355,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4801,25 +4648,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5165,25 +4993,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5442,25 +5251,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5756,25 +5546,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6062,25 +5833,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6370,25 +6122,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6654,25 +6387,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6966,25 +6680,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7330,25 +7025,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7611,25 +7287,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7920,25 +7577,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8284,25 +7922,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8566,25 +8185,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8713,12 +8313,6 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
@ -9410,7 +9004,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd-applicationset:latest
image: quay.io/argoproj/argocd:latest
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:

View file

@ -6,6 +6,7 @@ resources:
- ../cluster-rbac/application-controller
- ../base/config
- ../base/application-controller
- ../base/applicationset-controller
- ../base/repo-server
- ../base/redis
images:

File diff suppressed because it is too large Load diff

View file

@ -4,3 +4,4 @@ kind: Kustomization
resources:
- application-crd.yaml
- appproject-crd.yaml
- applicationset-crd.yaml

View file

@ -14,6 +14,7 @@ images:
newTag: latest
resources:
- ../../base/application-controller
- ../../base/applicationset-controller
- ../../base/dex
- ../../base/repo-server
- ../../base/server

View file

@ -2153,9 +2153,8 @@ spec:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.3.0
creationTimestamp: null
labels:
app.kubernetes.io/name: applicationsets.argoproj.io
name: applicationsets.argoproj.io
spec:
group: argoproj.io
@ -2379,25 +2378,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -2685,25 +2665,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -2993,25 +2954,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3277,25 +3219,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3591,25 +3514,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3897,25 +3801,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4205,25 +4090,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4489,25 +4355,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4801,25 +4648,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5165,25 +4993,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5442,25 +5251,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5756,25 +5546,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6062,25 +5833,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6370,25 +6122,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6654,25 +6387,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6966,25 +6680,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7330,25 +7025,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7611,25 +7287,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7920,25 +7577,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8284,25 +7922,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8566,25 +8185,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8713,12 +8313,6 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
@ -10347,7 +9941,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd-applicationset:latest
image: quay.io/argoproj/argocd:latest
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:

File diff suppressed because it is too large Load diff

View file

@ -2153,9 +2153,8 @@ spec:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.3.0
creationTimestamp: null
labels:
app.kubernetes.io/name: applicationsets.argoproj.io
name: applicationsets.argoproj.io
spec:
group: argoproj.io
@ -2379,25 +2378,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -2685,25 +2665,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -2993,25 +2954,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3277,25 +3219,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3591,25 +3514,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -3897,25 +3801,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4205,25 +4090,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4489,25 +4355,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -4801,25 +4648,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5165,25 +4993,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5442,25 +5251,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -5756,25 +5546,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6062,25 +5833,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6370,25 +6122,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6654,25 +6387,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -6966,25 +6680,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7330,25 +7025,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7611,25 +7287,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -7920,25 +7577,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8284,25 +7922,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8566,25 +8185,6 @@ spec:
version:
type: string
type: object
ksonnet:
properties:
environment:
type: string
parameters:
items:
properties:
component:
type: string
name:
type: string
value:
type: string
required:
- name
- value
type: object
type: array
type: object
kustomize:
properties:
commonAnnotations:
@ -8713,12 +8313,6 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
@ -9717,7 +9311,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: quay.io/argoproj/argocd-applicationset:latest
image: quay.io/argoproj/argocd:latest
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:

File diff suppressed because it is too large Load diff

View file

@ -17,4 +17,11 @@ const (
AppProjectPlural string = "appprojects"
AppProjectShortName string = "appproject"
AppProjectFullName string = AppProjectPlural + "." + Group
// AppProject constants
ApplicationSetKind string = "Applicationset"
ApplicationSetSingular string = "applicationset"
ApplicationSetPlural string = "applicationsets"
ApplicationSetShortName string = "appset"
ApplicationSetFullName string = ApplicationSetPlural + "." + Group
)

View file

@ -0,0 +1,502 @@
/*
Licensed 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 v1alpha1
import (
"encoding/json"
"fmt"
"sort"
"github.com/argoproj/argo-cd/v2/common"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
// Utility struct for a reference to a secret key.
type SecretRef struct {
SecretName string `json:"secretName"`
Key string `json:"key"`
}
// ApplicationSet is a set of Application resources
// +kubebuilder:object:root=true
// +kubebuilder:resource:path=applicationsets,shortName=appset;appsets
// +kubebuilder:subresource:status
type ApplicationSet struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
Spec ApplicationSetSpec `json:"spec"`
Status ApplicationSetStatus `json:"status,omitempty"`
}
// ApplicationSetSpec represents a class of application set state.
type ApplicationSetSpec struct {
Generators []ApplicationSetGenerator `json:"generators"`
Template ApplicationSetTemplate `json:"template"`
SyncPolicy *ApplicationSetSyncPolicy `json:"syncPolicy,omitempty"`
}
// ApplicationSetSyncPolicy configures how generated Applications will relate to their
// ApplicationSet.
type ApplicationSetSyncPolicy struct {
// PreserveResourcesOnDeletion will preserve resources on deletion. If PreserveResourcesOnDeletion is set to true, these Applications will not be deleted.
PreserveResourcesOnDeletion bool `json:"preserveResourcesOnDeletion,omitempty"`
}
// ApplicationSetTemplate represents argocd ApplicationSpec
type ApplicationSetTemplate struct {
ApplicationSetTemplateMeta `json:"metadata"`
Spec v1alpha1.ApplicationSpec `json:"spec"`
}
// ApplicationSetTemplateMeta represents the Argo CD application fields that may
// be used for Applications generated from the ApplicationSet (based on metav1.ObjectMeta)
type ApplicationSetTemplateMeta struct {
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Finalizers []string `json:"finalizers,omitempty"`
}
// ApplicationSetGenerator represents a generator at the top level of an ApplicationSet.
type ApplicationSetGenerator struct {
List *ListGenerator `json:"list,omitempty"`
Clusters *ClusterGenerator `json:"clusters,omitempty"`
Git *GitGenerator `json:"git,omitempty"`
SCMProvider *SCMProviderGenerator `json:"scmProvider,omitempty"`
ClusterDecisionResource *DuckTypeGenerator `json:"clusterDecisionResource,omitempty"`
PullRequest *PullRequestGenerator `json:"pullRequest,omitempty"`
Matrix *MatrixGenerator `json:"matrix,omitempty"`
Merge *MergeGenerator `json:"merge,omitempty"`
}
// ApplicationSetNestedGenerator represents a generator nested within a combination-type generator (MatrixGenerator or
// MergeGenerator).
type ApplicationSetNestedGenerator struct {
List *ListGenerator `json:"list,omitempty"`
Clusters *ClusterGenerator `json:"clusters,omitempty"`
Git *GitGenerator `json:"git,omitempty"`
SCMProvider *SCMProviderGenerator `json:"scmProvider,omitempty"`
ClusterDecisionResource *DuckTypeGenerator `json:"clusterDecisionResource,omitempty"`
PullRequest *PullRequestGenerator `json:"pullRequest,omitempty"`
// Matrix should have the form of NestedMatrixGenerator
Matrix *apiextensionsv1.JSON `json:"matrix,omitempty"`
// Merge should have the form of NestedMergeGenerator
Merge *apiextensionsv1.JSON `json:"merge,omitempty"`
}
type ApplicationSetNestedGenerators []ApplicationSetNestedGenerator
// ApplicationSetTerminalGenerator represents a generator nested within a nested generator (for example, a list within
// a merge within a matrix). A generator at this level may not be a combination-type generator (MatrixGenerator or
// MergeGenerator). ApplicationSet enforces this nesting depth limit because CRDs do not support recursive types.
// https://github.com/kubernetes-sigs/controller-tools/issues/477
type ApplicationSetTerminalGenerator struct {
List *ListGenerator `json:"list,omitempty"`
Clusters *ClusterGenerator `json:"clusters,omitempty"`
Git *GitGenerator `json:"git,omitempty"`
SCMProvider *SCMProviderGenerator `json:"scmProvider,omitempty"`
ClusterDecisionResource *DuckTypeGenerator `json:"clusterDecisionResource,omitempty"`
PullRequest *PullRequestGenerator `json:"pullRequest,omitempty"`
}
type ApplicationSetTerminalGenerators []ApplicationSetTerminalGenerator
// toApplicationSetNestedGenerators converts a terminal generator (a generator which cannot be a combination-type
// generator) to a "nested" generator. The conversion is for convenience, allowing generator g to be used where a nested
// generator is expected.
func (g ApplicationSetTerminalGenerators) toApplicationSetNestedGenerators() []ApplicationSetNestedGenerator {
nestedGenerators := make([]ApplicationSetNestedGenerator, len(g))
for i, terminalGenerator := range g {
nestedGenerators[i] = ApplicationSetNestedGenerator{
List: terminalGenerator.List,
Clusters: terminalGenerator.Clusters,
Git: terminalGenerator.Git,
SCMProvider: terminalGenerator.SCMProvider,
ClusterDecisionResource: terminalGenerator.ClusterDecisionResource,
PullRequest: terminalGenerator.PullRequest,
}
}
return nestedGenerators
}
// ListGenerator include items info
type ListGenerator struct {
Elements []apiextensionsv1.JSON `json:"elements"`
Template ApplicationSetTemplate `json:"template,omitempty"`
}
// MatrixGenerator generates the cartesian product of two sets of parameters. The parameters are defined by two nested
// generators.
type MatrixGenerator struct {
Generators []ApplicationSetNestedGenerator `json:"generators"`
Template ApplicationSetTemplate `json:"template,omitempty"`
}
// NestedMatrixGenerator is a MatrixGenerator nested under another combination-type generator (MatrixGenerator or
// MergeGenerator). NestedMatrixGenerator does not have an override template, because template overriding has no meaning
// within the constituent generators of combination-type generators.
//
// NOTE: Nested matrix generator is not included directly in the CRD struct, instead it is included
// as a generic 'apiextensionsv1.JSON' object, and then marshalled into a NestedMatrixGenerator
// when processed.
type NestedMatrixGenerator struct {
Generators ApplicationSetTerminalGenerators `json:"generators"`
}
// ToNestedMatrixGenerator converts a JSON struct (from the K8s resource) to corresponding
// NestedMatrixGenerator object.
func ToNestedMatrixGenerator(j *apiextensionsv1.JSON) (*NestedMatrixGenerator, error) {
if j == nil {
return nil, nil
}
nestedMatrixGenerator := NestedMatrixGenerator{}
err := json.Unmarshal(j.Raw, &nestedMatrixGenerator)
if err != nil {
return nil, err
}
return &nestedMatrixGenerator, nil
}
// ToMatrixGenerator converts a NestedMatrixGenerator to a MatrixGenerator. This conversion is for convenience, allowing
// a NestedMatrixGenerator to be used where a MatrixGenerator is expected (of course, the converted generator will have
// no override template).
func (g NestedMatrixGenerator) ToMatrixGenerator() *MatrixGenerator {
return &MatrixGenerator{
Generators: g.Generators.toApplicationSetNestedGenerators(),
}
}
// MergeGenerator merges the output of two or more generators. Where the values for all specified merge keys are equal
// between two sets of generated parameters, the parameter sets will be merged with the parameters from the latter
// generator taking precedence. Parameter sets with merge keys not present in the base generator's params will be
// ignored.
// For example, if the first generator produced [{a: '1', b: '2'}, {c: '1', d: '1'}] and the second generator produced
// [{'a': 'override'}], the united parameters for merge keys = ['a'] would be
// [{a: 'override', b: '1'}, {c: '1', d: '1'}].
//
// MergeGenerator supports template overriding. If a MergeGenerator is one of multiple top-level generators, its
// template will be merged with the top-level generator before the parameters are applied.
type MergeGenerator struct {
Generators []ApplicationSetNestedGenerator `json:"generators"`
MergeKeys []string `json:"mergeKeys"`
Template ApplicationSetTemplate `json:"template,omitempty"`
}
// NestedMergeGenerator is a MergeGenerator nested under another combination-type generator (MatrixGenerator or
// MergeGenerator). NestedMergeGenerator does not have an override template, because template overriding has no meaning
// within the constituent generators of combination-type generators.
//
// NOTE: Nested merge generator is not included directly in the CRD struct, instead it is included
// as a generic 'apiextensionsv1.JSON' object, and then marshalled into a NestedMergeGenerator
// when processed.
type NestedMergeGenerator struct {
Generators ApplicationSetTerminalGenerators `json:"generators"`
MergeKeys []string `json:"mergeKeys"`
}
// ToNestedMergeGenerator converts a JSON struct (from the K8s resource) to corresponding
// NestedMergeGenerator object.
func ToNestedMergeGenerator(j *apiextensionsv1.JSON) (*NestedMergeGenerator, error) {
if j == nil {
return nil, nil
}
nestedMergeGenerator := NestedMergeGenerator{}
err := json.Unmarshal(j.Raw, &nestedMergeGenerator)
if err != nil {
return nil, err
}
return &nestedMergeGenerator, nil
}
// ToMergeGenerator converts a NestedMergeGenerator to a MergeGenerator. This conversion is for convenience, allowing
// a NestedMergeGenerator to be used where a MergeGenerator is expected (of course, the converted generator will have
// no override template).
func (g NestedMergeGenerator) ToMergeGenerator() *MergeGenerator {
return &MergeGenerator{
Generators: g.Generators.toApplicationSetNestedGenerators(),
MergeKeys: g.MergeKeys,
}
}
// ClusterGenerator defines a generator to match against clusters registered with ArgoCD.
type ClusterGenerator struct {
// Selector defines a label selector to match against all clusters registered with ArgoCD.
// Clusters today are stored as Kubernetes Secrets, thus the Secret labels will be used
// for matching the selector.
Selector metav1.LabelSelector `json:"selector,omitempty"`
Template ApplicationSetTemplate `json:"template,omitempty"`
// Values contains key/value pairs which are passed directly as parameters to the template
Values map[string]string `json:"values,omitempty"`
}
// DuckType defines a generator to match against clusters registered with ArgoCD.
type DuckTypeGenerator struct {
// ConfigMapRef is a ConfigMap with the duck type definitions needed to retrieve the data
// this includes apiVersion(group/version), kind, matchKey and validation settings
// Name is the resource name of the kind, group and version, defined in the ConfigMapRef
// RequeueAfterSeconds is how long before the duckType will be rechecked for a change
ConfigMapRef string `json:"configMapRef"`
Name string `json:"name,omitempty"`
RequeueAfterSeconds *int64 `json:"requeueAfterSeconds,omitempty"`
LabelSelector metav1.LabelSelector `json:"labelSelector,omitempty"`
Template ApplicationSetTemplate `json:"template,omitempty"`
// Values contains key/value pairs which are passed directly as parameters to the template
Values map[string]string `json:"values,omitempty"`
}
type GitGenerator struct {
RepoURL string `json:"repoURL"`
Directories []GitDirectoryGeneratorItem `json:"directories,omitempty"`
Files []GitFileGeneratorItem `json:"files,omitempty"`
Revision string `json:"revision"`
RequeueAfterSeconds *int64 `json:"requeueAfterSeconds,omitempty"`
Template ApplicationSetTemplate `json:"template,omitempty"`
}
type GitDirectoryGeneratorItem struct {
Path string `json:"path"`
Exclude bool `json:"exclude,omitempty"`
}
type GitFileGeneratorItem struct {
Path string `json:"path"`
}
// SCMProviderGenerator defines a generator that scrapes a SCMaaS API to find candidate repos.
type SCMProviderGenerator struct {
// Which provider to use and config for it.
Github *SCMProviderGeneratorGithub `json:"github,omitempty"`
Gitlab *SCMProviderGeneratorGitlab `json:"gitlab,omitempty"`
Bitbucket *SCMProviderGeneratorBitbucket `json:"bitbucket,omitempty"`
// Filters for which repos should be considered.
Filters []SCMProviderGeneratorFilter `json:"filters,omitempty"`
// Which protocol to use for the SCM URL. Default is provider-specific but ssh if possible. Not all providers
// necessarily support all protocols.
CloneProtocol string `json:"cloneProtocol,omitempty"`
// Standard parameters.
RequeueAfterSeconds *int64 `json:"requeueAfterSeconds,omitempty"`
Template ApplicationSetTemplate `json:"template,omitempty"`
}
// SCMProviderGeneratorGithub defines a connection info specific to GitHub.
type SCMProviderGeneratorGithub struct {
// GitHub org to scan. Required.
Organization string `json:"organization"`
// The GitHub API URL to talk to. If blank, use https://api.github.com/.
API string `json:"api,omitempty"`
// Authentication token reference.
TokenRef *SecretRef `json:"tokenRef,omitempty"`
// Scan all branches instead of just the default branch.
AllBranches bool `json:"allBranches,omitempty"`
}
// SCMProviderGeneratorGitlab defines a connection info specific to Gitlab.
type SCMProviderGeneratorGitlab struct {
// Gitlab group to scan. Required. You can use either the project id (recommended) or the full namespaced path.
Group string `json:"group"`
// Recurse through subgroups (true) or scan only the base group (false). Defaults to "false"
IncludeSubgroups bool `json:"includeSubgroups,omitempty"`
// The Gitlab API URL to talk to.
API string `json:"api,omitempty"`
// Authentication token reference.
TokenRef *SecretRef `json:"tokenRef,omitempty"`
// Scan all branches instead of just the default branch.
AllBranches bool `json:"allBranches,omitempty"`
}
// SCMProviderGeneratorBitbucket defines connection info specific to Bitbucket Cloud (API version 2).
type SCMProviderGeneratorBitbucket struct {
// Bitbucket workspace to scan. Required.
Owner string `json:"owner"`
// Bitbucket user to use when authenticating. Should have a "member" role to be able to read all repositories and branches. Required
User string `json:"user"`
// The app password to use for the user. Required. See: https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/
AppPasswordRef *SecretRef `json:"appPasswordRef"`
// Scan all branches instead of just the main branch.
AllBranches bool `json:"allBranches,omitempty"`
}
// SCMProviderGeneratorFilter is a single repository filter.
// If multiple filter types are set on a single struct, they will be AND'd together. All filters must
// pass for a repo to be included.
type SCMProviderGeneratorFilter struct {
// A regex for repo names.
RepositoryMatch *string `json:"repositoryMatch,omitempty"`
// An array of paths, all of which must exist.
PathsExist []string `json:"pathsExist,omitempty"`
// A regex which must match at least one label.
LabelMatch *string `json:"labelMatch,omitempty"`
// A regex which must match the branch name.
BranchMatch *string `json:"branchMatch,omitempty"`
}
// PullRequestGenerator defines a generator that scrapes a PullRequest API to find candidate pull requests.
type PullRequestGenerator struct {
// Which provider to use and config for it.
Github *PullRequestGeneratorGithub `json:"github,omitempty"`
// Standard parameters.
RequeueAfterSeconds *int64 `json:"requeueAfterSeconds,omitempty"`
Template ApplicationSetTemplate `json:"template,omitempty"`
}
// PullRequestGenerator defines a connection info specific to GitHub.
type PullRequestGeneratorGithub struct {
// GitHub org or user to scan. Required.
Owner string `json:"owner"`
// GitHub repo name to scan. Required.
Repo string `json:"repo"`
// The GitHub API URL to talk to. If blank, use https://api.github.com/.
API string `json:"api,omitempty"`
// Authentication token reference.
TokenRef *SecretRef `json:"tokenRef,omitempty"`
// Labels is used to filter the PRs that you want to target
Labels []string `json:"labels,omitempty"`
}
// ApplicationSetStatus defines the observed state of ApplicationSet
type ApplicationSetStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Conditions []ApplicationSetCondition `json:"conditions,omitempty"`
}
// ApplicationSetCondition contains details about an applicationset condition, which is usally an error or warning
type ApplicationSetCondition struct {
// Type is an applicationset condition type
Type ApplicationSetConditionType `json:"type" protobuf:"bytes,1,opt,name=type"`
// Message contains human-readable message indicating details about condition
Message string `json:"message" protobuf:"bytes,2,opt,name=message"`
// LastTransitionTime is the time the condition was last observed
LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,3,opt,name=lastTransitionTime"`
// True/False/Unknown
Status ApplicationSetConditionStatus `json:"status" protobuf:"bytes,4,opt,name=status"`
//Single word camelcase representing the reason for the status eg ErrorOccurred
Reason string `json:"reason" protobuf:"bytes,5,opt,name=reason"`
}
// SyncStatusCode is a type which represents possible comparison results
type ApplicationSetConditionStatus string
// Application Condition Status
const (
// ApplicationSetConditionStatusTrue indicates that a application has been successfully established
ApplicationSetConditionStatusTrue ApplicationSetConditionStatus = "True"
// ApplicationSetConditionStatusFalse indicates that a application attempt has failed
ApplicationSetConditionStatusFalse ApplicationSetConditionStatus = "False"
// ApplicationSetConditionStatusUnknown indicates that the application condition status could not be reliably determined
ApplicationSetConditionStatusUnknown ApplicationSetConditionStatus = "Unknown"
)
// ApplicationSetConditionType represents type of application condition. Type name has following convention:
// prefix "Error" means error condition
// prefix "Warning" means warning condition
// prefix "Info" means informational condition
type ApplicationSetConditionType string
//ErrorOccurred / ParametersGenerated / TemplateRendered / ResourcesUpToDate
const (
ApplicationSetConditionErrorOccurred ApplicationSetConditionType = "ErrorOccurred"
ApplicationSetConditionParametersGenerated ApplicationSetConditionType = "ParametersGenerated"
ApplicationSetConditionResourcesUpToDate ApplicationSetConditionType = "ResourcesUpToDate"
)
type ApplicationSetReasonType string
const (
ApplicationSetReasonErrorOccurred = "ErrorOccurred"
ApplicationSetReasonApplicationSetUpToDate = "ApplicationSetUpToDate"
ApplicationSetReasonParametersGenerated = "ParametersGenerated"
ApplicationSetReasonApplicationGenerated = "ApplicationGeneratedSuccessfully"
ApplicationSetReasonUpdateApplicationError = "UpdateApplicationError"
ApplicationSetReasonApplicationParamsGenerationError = "ApplicationGenerationFromParamsError"
ApplicationSetReasonRenderTemplateParamsError = "RenderTemplateParamsError"
ApplicationSetReasonCreateApplicationError = "CreateApplicationError"
ApplicationSetReasonDeleteApplicationError = "DeleteApplicationError"
ApplicationSetReasonRefreshApplicationError = "RefreshApplicationError"
ApplicationSetReasonApplicationValidationError = "ApplicationValidationError"
)
// ApplicationSetList contains a list of ApplicationSet
// +kubebuilder:object:root=true
type ApplicationSetList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ApplicationSet `json:"items"`
}
func init() {
SchemeBuilder.Register(&ApplicationSet{}, &ApplicationSetList{})
}
// RefreshRequired checks if the ApplicationSet needs to be refreshed
func (a *ApplicationSet) RefreshRequired() bool {
_, found := a.Annotations[common.AnnotationApplicationSetRefresh]
return found
}
// SetConditions updates the applicationset status conditions for a subset of evaluated types.
// If the applicationset has a pre-existing condition of a type that is not in the evaluated list,
// it will be preserved. If the applicationset has a pre-existing condition of a type, status, reason that
// is in the evaluated list, but not in the incoming conditions list, it will be removed.
func (status *ApplicationSetStatus) SetConditions(conditions []ApplicationSetCondition, evaluatedTypes map[ApplicationSetConditionType]bool) {
applicationSetConditions := make([]ApplicationSetCondition, 0)
now := metav1.Now()
for i := range conditions {
condition := conditions[i]
if condition.LastTransitionTime == nil {
condition.LastTransitionTime = &now
}
eci := findConditionIndex(status.Conditions, condition.Type)
if eci >= 0 && status.Conditions[eci].Message == condition.Message && status.Conditions[eci].Reason == condition.Reason && status.Conditions[eci].Status == condition.Status {
// If we already have a condition of this type, status and reason, only update the timestamp if something
// has changed.
applicationSetConditions = append(applicationSetConditions, status.Conditions[eci])
} else {
// Otherwise we use the new incoming condition with an updated timestamp:
applicationSetConditions = append(applicationSetConditions, condition)
}
}
sort.Slice(applicationSetConditions, func(i, j int) bool {
left := applicationSetConditions[i]
right := applicationSetConditions[j]
return fmt.Sprintf("%s/%s/%s/%s/%v", left.Type, left.Message, left.Status, left.Reason, left.LastTransitionTime) < fmt.Sprintf("%s/%s/%s/%s/%v", right.Type, right.Message, right.Status, right.Reason, right.LastTransitionTime)
})
status.Conditions = applicationSetConditions
}
func findConditionIndex(conditions []ApplicationSetCondition, t ApplicationSetConditionType) int {
for i := range conditions {
if conditions[i].Type == t {
return i
}
}
return -1
}

View file

@ -0,0 +1,133 @@
package v1alpha1
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func testCond(t ApplicationSetConditionType, msg string, lastTransitionTime *metav1.Time, status ApplicationSetConditionStatus, reason string) ApplicationSetCondition {
return ApplicationSetCondition{
Type: t,
Message: msg,
LastTransitionTime: lastTransitionTime,
Status: status,
Reason: reason,
}
}
func newTestAppSet(name, namespace, repo string) *ApplicationSet {
a := &ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: ApplicationSetSpec{
Generators: []ApplicationSetGenerator{
{
Git: &GitGenerator{
RepoURL: repo,
},
},
},
},
}
return a
}
func TestSetConditions(t *testing.T) {
fiveMinsAgo := &metav1.Time{Time: time.Now().Add(-5 * time.Minute)}
tenMinsAgo := &metav1.Time{Time: time.Now().Add(-10 * time.Minute)}
tests := []struct {
name string
existing []ApplicationSetCondition
incoming []ApplicationSetCondition
evaluatedTypes map[ApplicationSetConditionType]bool
expected []ApplicationSetCondition
validate func(*testing.T, *ApplicationSet)
}{
{
name: "new conditions with lastTransitionTime",
existing: []ApplicationSetCondition{},
incoming: []ApplicationSetCondition{
testCond(ApplicationSetConditionErrorOccurred, "foo", fiveMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
},
evaluatedTypes: map[ApplicationSetConditionType]bool{
ApplicationSetConditionErrorOccurred: true,
ApplicationSetConditionResourcesUpToDate: true,
},
expected: []ApplicationSetCondition{
testCond(ApplicationSetConditionErrorOccurred, "foo", fiveMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
},
validate: func(t *testing.T, a *ApplicationSet) {
assert.Equal(t, fiveMinsAgo, a.Status.Conditions[0].LastTransitionTime)
assert.Equal(t, tenMinsAgo, a.Status.Conditions[1].LastTransitionTime)
},
}, {
name: "new conditions without lastTransitionTime",
existing: []ApplicationSetCondition{},
incoming: []ApplicationSetCondition{
testCond(ApplicationSetConditionErrorOccurred, "foo", nil, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
testCond(ApplicationSetConditionResourcesUpToDate, "bar", nil, ApplicationSetConditionStatusFalse, ApplicationSetReasonApplicationSetUpToDate),
},
evaluatedTypes: map[ApplicationSetConditionType]bool{
ApplicationSetConditionErrorOccurred: true,
ApplicationSetConditionResourcesUpToDate: true,
},
expected: []ApplicationSetCondition{
testCond(ApplicationSetConditionErrorOccurred, "foo", nil, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
testCond(ApplicationSetConditionResourcesUpToDate, "bar", nil, ApplicationSetConditionStatusFalse, ApplicationSetReasonApplicationSetUpToDate),
},
validate: func(t *testing.T, a *ApplicationSet) {
// SetConditions should add timestamps for new conditions.
assert.True(t, a.Status.Conditions[0].LastTransitionTime.Time.After(fiveMinsAgo.Time))
assert.True(t, a.Status.Conditions[1].LastTransitionTime.Time.After(fiveMinsAgo.Time))
},
}, {
name: "condition cleared",
existing: []ApplicationSetCondition{
testCond(ApplicationSetConditionErrorOccurred, "foo", fiveMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusFalse, ApplicationSetReasonApplicationSetUpToDate),
},
incoming: []ApplicationSetCondition{
testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
},
evaluatedTypes: map[ApplicationSetConditionType]bool{
ApplicationSetConditionErrorOccurred: true,
ApplicationSetConditionResourcesUpToDate: true,
},
expected: []ApplicationSetCondition{
testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
},
validate: func(t *testing.T, a *ApplicationSet) {
assert.Equal(t, tenMinsAgo.Time, a.Status.Conditions[0].LastTransitionTime.Time)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testRepo := "https://github.com/org/repo"
namespace := "test"
a := newTestAppSet("sample-app-set", namespace, testRepo)
a.Status.Conditions = tt.existing
a.Status.SetConditions(tt.incoming, tt.evaluatedTypes)
assertConditions(t, tt.expected, a.Status.Conditions)
if tt.validate != nil {
tt.validate(t, a)
}
})
}
}
func assertConditions(t *testing.T, expected []ApplicationSetCondition, actual []ApplicationSetCondition) {
assert.Equal(t, len(expected), len(actual))
for i := range expected {
assert.Equal(t, expected[i].Type, actual[i].Type)
assert.Equal(t, expected[i].Message, actual[i].Message)
}
}

View file

@ -0,0 +1,36 @@
/*
Licensed 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 v1alpha1 contains API Schema definitions for the argoproj.io v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=argoproj.io
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "argoproj.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View file

@ -0,0 +1,857 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Licensed 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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSet) DeepCopyInto(out *ApplicationSet) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSet.
func (in *ApplicationSet) DeepCopy() *ApplicationSet {
if in == nil {
return nil
}
out := new(ApplicationSet)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ApplicationSet) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetCondition) DeepCopyInto(out *ApplicationSetCondition) {
*out = *in
if in.LastTransitionTime != nil {
in, out := &in.LastTransitionTime, &out.LastTransitionTime
*out = (*in).DeepCopy()
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetCondition.
func (in *ApplicationSetCondition) DeepCopy() *ApplicationSetCondition {
if in == nil {
return nil
}
out := new(ApplicationSetCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetGenerator) DeepCopyInto(out *ApplicationSetGenerator) {
*out = *in
if in.List != nil {
in, out := &in.List, &out.List
*out = new(ListGenerator)
(*in).DeepCopyInto(*out)
}
if in.Clusters != nil {
in, out := &in.Clusters, &out.Clusters
*out = new(ClusterGenerator)
(*in).DeepCopyInto(*out)
}
if in.Git != nil {
in, out := &in.Git, &out.Git
*out = new(GitGenerator)
(*in).DeepCopyInto(*out)
}
if in.SCMProvider != nil {
in, out := &in.SCMProvider, &out.SCMProvider
*out = new(SCMProviderGenerator)
(*in).DeepCopyInto(*out)
}
if in.ClusterDecisionResource != nil {
in, out := &in.ClusterDecisionResource, &out.ClusterDecisionResource
*out = new(DuckTypeGenerator)
(*in).DeepCopyInto(*out)
}
if in.PullRequest != nil {
in, out := &in.PullRequest, &out.PullRequest
*out = new(PullRequestGenerator)
(*in).DeepCopyInto(*out)
}
if in.Matrix != nil {
in, out := &in.Matrix, &out.Matrix
*out = new(MatrixGenerator)
(*in).DeepCopyInto(*out)
}
if in.Merge != nil {
in, out := &in.Merge, &out.Merge
*out = new(MergeGenerator)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetGenerator.
func (in *ApplicationSetGenerator) DeepCopy() *ApplicationSetGenerator {
if in == nil {
return nil
}
out := new(ApplicationSetGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetList) DeepCopyInto(out *ApplicationSetList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ApplicationSet, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetList.
func (in *ApplicationSetList) DeepCopy() *ApplicationSetList {
if in == nil {
return nil
}
out := new(ApplicationSetList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ApplicationSetList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetNestedGenerator) DeepCopyInto(out *ApplicationSetNestedGenerator) {
*out = *in
if in.List != nil {
in, out := &in.List, &out.List
*out = new(ListGenerator)
(*in).DeepCopyInto(*out)
}
if in.Clusters != nil {
in, out := &in.Clusters, &out.Clusters
*out = new(ClusterGenerator)
(*in).DeepCopyInto(*out)
}
if in.Git != nil {
in, out := &in.Git, &out.Git
*out = new(GitGenerator)
(*in).DeepCopyInto(*out)
}
if in.SCMProvider != nil {
in, out := &in.SCMProvider, &out.SCMProvider
*out = new(SCMProviderGenerator)
(*in).DeepCopyInto(*out)
}
if in.ClusterDecisionResource != nil {
in, out := &in.ClusterDecisionResource, &out.ClusterDecisionResource
*out = new(DuckTypeGenerator)
(*in).DeepCopyInto(*out)
}
if in.PullRequest != nil {
in, out := &in.PullRequest, &out.PullRequest
*out = new(PullRequestGenerator)
(*in).DeepCopyInto(*out)
}
if in.Matrix != nil {
in, out := &in.Matrix, &out.Matrix
*out = new(v1.JSON)
(*in).DeepCopyInto(*out)
}
if in.Merge != nil {
in, out := &in.Merge, &out.Merge
*out = new(v1.JSON)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetNestedGenerator.
func (in *ApplicationSetNestedGenerator) DeepCopy() *ApplicationSetNestedGenerator {
if in == nil {
return nil
}
out := new(ApplicationSetNestedGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ApplicationSetNestedGenerators) DeepCopyInto(out *ApplicationSetNestedGenerators) {
{
in := &in
*out = make(ApplicationSetNestedGenerators, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetNestedGenerators.
func (in ApplicationSetNestedGenerators) DeepCopy() ApplicationSetNestedGenerators {
if in == nil {
return nil
}
out := new(ApplicationSetNestedGenerators)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetSpec) DeepCopyInto(out *ApplicationSetSpec) {
*out = *in
if in.Generators != nil {
in, out := &in.Generators, &out.Generators
*out = make([]ApplicationSetGenerator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Template.DeepCopyInto(&out.Template)
if in.SyncPolicy != nil {
in, out := &in.SyncPolicy, &out.SyncPolicy
*out = new(ApplicationSetSyncPolicy)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetSpec.
func (in *ApplicationSetSpec) DeepCopy() *ApplicationSetSpec {
if in == nil {
return nil
}
out := new(ApplicationSetSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetStatus) DeepCopyInto(out *ApplicationSetStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]ApplicationSetCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetStatus.
func (in *ApplicationSetStatus) DeepCopy() *ApplicationSetStatus {
if in == nil {
return nil
}
out := new(ApplicationSetStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetSyncPolicy) DeepCopyInto(out *ApplicationSetSyncPolicy) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetSyncPolicy.
func (in *ApplicationSetSyncPolicy) DeepCopy() *ApplicationSetSyncPolicy {
if in == nil {
return nil
}
out := new(ApplicationSetSyncPolicy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetTemplate) DeepCopyInto(out *ApplicationSetTemplate) {
*out = *in
in.ApplicationSetTemplateMeta.DeepCopyInto(&out.ApplicationSetTemplateMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetTemplate.
func (in *ApplicationSetTemplate) DeepCopy() *ApplicationSetTemplate {
if in == nil {
return nil
}
out := new(ApplicationSetTemplate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetTemplateMeta) DeepCopyInto(out *ApplicationSetTemplateMeta) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Finalizers != nil {
in, out := &in.Finalizers, &out.Finalizers
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetTemplateMeta.
func (in *ApplicationSetTemplateMeta) DeepCopy() *ApplicationSetTemplateMeta {
if in == nil {
return nil
}
out := new(ApplicationSetTemplateMeta)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSetTerminalGenerator) DeepCopyInto(out *ApplicationSetTerminalGenerator) {
*out = *in
if in.List != nil {
in, out := &in.List, &out.List
*out = new(ListGenerator)
(*in).DeepCopyInto(*out)
}
if in.Clusters != nil {
in, out := &in.Clusters, &out.Clusters
*out = new(ClusterGenerator)
(*in).DeepCopyInto(*out)
}
if in.Git != nil {
in, out := &in.Git, &out.Git
*out = new(GitGenerator)
(*in).DeepCopyInto(*out)
}
if in.SCMProvider != nil {
in, out := &in.SCMProvider, &out.SCMProvider
*out = new(SCMProviderGenerator)
(*in).DeepCopyInto(*out)
}
if in.ClusterDecisionResource != nil {
in, out := &in.ClusterDecisionResource, &out.ClusterDecisionResource
*out = new(DuckTypeGenerator)
(*in).DeepCopyInto(*out)
}
if in.PullRequest != nil {
in, out := &in.PullRequest, &out.PullRequest
*out = new(PullRequestGenerator)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetTerminalGenerator.
func (in *ApplicationSetTerminalGenerator) DeepCopy() *ApplicationSetTerminalGenerator {
if in == nil {
return nil
}
out := new(ApplicationSetTerminalGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ApplicationSetTerminalGenerators) DeepCopyInto(out *ApplicationSetTerminalGenerators) {
{
in := &in
*out = make(ApplicationSetTerminalGenerators, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetTerminalGenerators.
func (in ApplicationSetTerminalGenerators) DeepCopy() ApplicationSetTerminalGenerators {
if in == nil {
return nil
}
out := new(ApplicationSetTerminalGenerators)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterGenerator) DeepCopyInto(out *ClusterGenerator) {
*out = *in
in.Selector.DeepCopyInto(&out.Selector)
in.Template.DeepCopyInto(&out.Template)
if in.Values != nil {
in, out := &in.Values, &out.Values
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGenerator.
func (in *ClusterGenerator) DeepCopy() *ClusterGenerator {
if in == nil {
return nil
}
out := new(ClusterGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DuckTypeGenerator) DeepCopyInto(out *DuckTypeGenerator) {
*out = *in
if in.RequeueAfterSeconds != nil {
in, out := &in.RequeueAfterSeconds, &out.RequeueAfterSeconds
*out = new(int64)
**out = **in
}
in.LabelSelector.DeepCopyInto(&out.LabelSelector)
in.Template.DeepCopyInto(&out.Template)
if in.Values != nil {
in, out := &in.Values, &out.Values
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DuckTypeGenerator.
func (in *DuckTypeGenerator) DeepCopy() *DuckTypeGenerator {
if in == nil {
return nil
}
out := new(DuckTypeGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitDirectoryGeneratorItem) DeepCopyInto(out *GitDirectoryGeneratorItem) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitDirectoryGeneratorItem.
func (in *GitDirectoryGeneratorItem) DeepCopy() *GitDirectoryGeneratorItem {
if in == nil {
return nil
}
out := new(GitDirectoryGeneratorItem)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitFileGeneratorItem) DeepCopyInto(out *GitFileGeneratorItem) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitFileGeneratorItem.
func (in *GitFileGeneratorItem) DeepCopy() *GitFileGeneratorItem {
if in == nil {
return nil
}
out := new(GitFileGeneratorItem)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitGenerator) DeepCopyInto(out *GitGenerator) {
*out = *in
if in.Directories != nil {
in, out := &in.Directories, &out.Directories
*out = make([]GitDirectoryGeneratorItem, len(*in))
copy(*out, *in)
}
if in.Files != nil {
in, out := &in.Files, &out.Files
*out = make([]GitFileGeneratorItem, len(*in))
copy(*out, *in)
}
if in.RequeueAfterSeconds != nil {
in, out := &in.RequeueAfterSeconds, &out.RequeueAfterSeconds
*out = new(int64)
**out = **in
}
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitGenerator.
func (in *GitGenerator) DeepCopy() *GitGenerator {
if in == nil {
return nil
}
out := new(GitGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ListGenerator) DeepCopyInto(out *ListGenerator) {
*out = *in
if in.Elements != nil {
in, out := &in.Elements, &out.Elements
*out = make([]v1.JSON, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListGenerator.
func (in *ListGenerator) DeepCopy() *ListGenerator {
if in == nil {
return nil
}
out := new(ListGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MatrixGenerator) DeepCopyInto(out *MatrixGenerator) {
*out = *in
if in.Generators != nil {
in, out := &in.Generators, &out.Generators
*out = make([]ApplicationSetNestedGenerator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatrixGenerator.
func (in *MatrixGenerator) DeepCopy() *MatrixGenerator {
if in == nil {
return nil
}
out := new(MatrixGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MergeGenerator) DeepCopyInto(out *MergeGenerator) {
*out = *in
if in.Generators != nil {
in, out := &in.Generators, &out.Generators
*out = make([]ApplicationSetNestedGenerator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.MergeKeys != nil {
in, out := &in.MergeKeys, &out.MergeKeys
*out = make([]string, len(*in))
copy(*out, *in)
}
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeGenerator.
func (in *MergeGenerator) DeepCopy() *MergeGenerator {
if in == nil {
return nil
}
out := new(MergeGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NestedMatrixGenerator) DeepCopyInto(out *NestedMatrixGenerator) {
*out = *in
if in.Generators != nil {
in, out := &in.Generators, &out.Generators
*out = make(ApplicationSetTerminalGenerators, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NestedMatrixGenerator.
func (in *NestedMatrixGenerator) DeepCopy() *NestedMatrixGenerator {
if in == nil {
return nil
}
out := new(NestedMatrixGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NestedMergeGenerator) DeepCopyInto(out *NestedMergeGenerator) {
*out = *in
if in.Generators != nil {
in, out := &in.Generators, &out.Generators
*out = make(ApplicationSetTerminalGenerators, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.MergeKeys != nil {
in, out := &in.MergeKeys, &out.MergeKeys
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NestedMergeGenerator.
func (in *NestedMergeGenerator) DeepCopy() *NestedMergeGenerator {
if in == nil {
return nil
}
out := new(NestedMergeGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PullRequestGenerator) DeepCopyInto(out *PullRequestGenerator) {
*out = *in
if in.Github != nil {
in, out := &in.Github, &out.Github
*out = new(PullRequestGeneratorGithub)
(*in).DeepCopyInto(*out)
}
if in.RequeueAfterSeconds != nil {
in, out := &in.RequeueAfterSeconds, &out.RequeueAfterSeconds
*out = new(int64)
**out = **in
}
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PullRequestGenerator.
func (in *PullRequestGenerator) DeepCopy() *PullRequestGenerator {
if in == nil {
return nil
}
out := new(PullRequestGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PullRequestGeneratorGithub) DeepCopyInto(out *PullRequestGeneratorGithub) {
*out = *in
if in.TokenRef != nil {
in, out := &in.TokenRef, &out.TokenRef
*out = new(SecretRef)
**out = **in
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PullRequestGeneratorGithub.
func (in *PullRequestGeneratorGithub) DeepCopy() *PullRequestGeneratorGithub {
if in == nil {
return nil
}
out := new(PullRequestGeneratorGithub)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SCMProviderGenerator) DeepCopyInto(out *SCMProviderGenerator) {
*out = *in
if in.Github != nil {
in, out := &in.Github, &out.Github
*out = new(SCMProviderGeneratorGithub)
(*in).DeepCopyInto(*out)
}
if in.Gitlab != nil {
in, out := &in.Gitlab, &out.Gitlab
*out = new(SCMProviderGeneratorGitlab)
(*in).DeepCopyInto(*out)
}
if in.Bitbucket != nil {
in, out := &in.Bitbucket, &out.Bitbucket
*out = new(SCMProviderGeneratorBitbucket)
(*in).DeepCopyInto(*out)
}
if in.Filters != nil {
in, out := &in.Filters, &out.Filters
*out = make([]SCMProviderGeneratorFilter, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.RequeueAfterSeconds != nil {
in, out := &in.RequeueAfterSeconds, &out.RequeueAfterSeconds
*out = new(int64)
**out = **in
}
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SCMProviderGenerator.
func (in *SCMProviderGenerator) DeepCopy() *SCMProviderGenerator {
if in == nil {
return nil
}
out := new(SCMProviderGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SCMProviderGeneratorBitbucket) DeepCopyInto(out *SCMProviderGeneratorBitbucket) {
*out = *in
if in.AppPasswordRef != nil {
in, out := &in.AppPasswordRef, &out.AppPasswordRef
*out = new(SecretRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SCMProviderGeneratorBitbucket.
func (in *SCMProviderGeneratorBitbucket) DeepCopy() *SCMProviderGeneratorBitbucket {
if in == nil {
return nil
}
out := new(SCMProviderGeneratorBitbucket)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SCMProviderGeneratorFilter) DeepCopyInto(out *SCMProviderGeneratorFilter) {
*out = *in
if in.RepositoryMatch != nil {
in, out := &in.RepositoryMatch, &out.RepositoryMatch
*out = new(string)
**out = **in
}
if in.PathsExist != nil {
in, out := &in.PathsExist, &out.PathsExist
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.LabelMatch != nil {
in, out := &in.LabelMatch, &out.LabelMatch
*out = new(string)
**out = **in
}
if in.BranchMatch != nil {
in, out := &in.BranchMatch, &out.BranchMatch
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SCMProviderGeneratorFilter.
func (in *SCMProviderGeneratorFilter) DeepCopy() *SCMProviderGeneratorFilter {
if in == nil {
return nil
}
out := new(SCMProviderGeneratorFilter)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SCMProviderGeneratorGithub) DeepCopyInto(out *SCMProviderGeneratorGithub) {
*out = *in
if in.TokenRef != nil {
in, out := &in.TokenRef, &out.TokenRef
*out = new(SecretRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SCMProviderGeneratorGithub.
func (in *SCMProviderGeneratorGithub) DeepCopy() *SCMProviderGeneratorGithub {
if in == nil {
return nil
}
out := new(SCMProviderGeneratorGithub)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SCMProviderGeneratorGitlab) DeepCopyInto(out *SCMProviderGeneratorGitlab) {
*out = *in
if in.TokenRef != nil {
in, out := &in.TokenRef, &out.TokenRef
*out = new(SecretRef)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SCMProviderGeneratorGitlab.
func (in *SCMProviderGeneratorGitlab) DeepCopy() *SCMProviderGeneratorGitlab {
if in == nil {
return nil
}
out := new(SCMProviderGeneratorGitlab)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretRef) DeepCopyInto(out *SecretRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef.
func (in *SecretRef) DeepCopy() *SecretRef {
if in == nil {
return nil
}
out := new(SecretRef)
in.DeepCopyInto(out)
return out
}

View file

@ -136,7 +136,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) {
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src}
// update this value if we add/remove manifests
const countOfManifests = 41
const countOfManifests = 46
res1, err := service.GenerateManifest(context.Background(), &q)

View file

@ -10,3 +10,4 @@ fcgiwrap: sudo sh -c "test $ARGOCD_E2E_TEST = true && (fcgiwrap -s unix:/var/run
nginx: sudo sh -c "test $ARGOCD_E2E_TEST = true && nginx -g 'daemon off;' -c $(pwd)/test/fixture/testrepos/nginx.conf"
helm-registry: sudo sh -c "registry serve /etc/docker/registry/config.yml"
dev-mounter: test "$ARGOCD_E2E_TEST" != "true" && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
applicationset-controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_BINARY_NAME=argocd-applicationset-controller go run ./cmd/main.go --loglevel debug --metrics-addr localhost:12345 --probe-addr localhost:12346 --namespace argocd-e2e --loglevel debug --argocd-repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"

View file

@ -0,0 +1,616 @@
package e2e
import (
"strings"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
var (
ExpectedConditions = []v1alpha1.ApplicationSetCondition{
{
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: "Successfully generated parameters for all Applications",
Reason: v1alpha1.ApplicationSetReasonApplicationSetUpToDate,
},
{
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: "Successfully generated parameters for all Applications",
Reason: v1alpha1.ApplicationSetReasonParametersGenerated,
},
{
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: "ApplicationSet up to date",
Reason: v1alpha1.ApplicationSetReasonApplicationSetUpToDate,
},
}
)
func TestSimpleListGenerator(t *testing.T) {
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}
var expectedAppNewNamespace *argov1alpha1.Application
var expectedAppNewMetadata *argov1alpha1.Application
Given(t).
// Create a ListGenerator-based ApplicationSet
When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-list-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "{{url}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{
Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
}},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
And(func() {
expectedAppNewNamespace = expectedApp.DeepCopy()
expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
// Update the metadata fields in the appset template, and make sure it propagates to the apps
When().
And(func() {
expectedAppNewMetadata = expectedAppNewNamespace.DeepCopy()
expectedAppNewMetadata.ObjectMeta.Annotations = map[string]string{"annotation-key": "annotation-value"}
expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{"label-key": "label-value"}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Annotations = map[string]string{"annotation-key": "annotation-value"}
appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewMetadata})).
// verify the ApplicationSet status conditions were set correctly
Expect(ApplicationSetHasConditions("simple-list-generator", ExpectedConditions)).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewMetadata}))
}
func TestSimpleGitDirectoryGenerator(t *testing.T) {
generateExpectedApp := func(name string) argov1alpha1.Application {
return argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: name,
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: name,
},
},
}
}
expectedApps := []argov1alpha1.Application{
generateExpectedApp("kustomize-guestbook"),
generateExpectedApp("helm-guestbook"),
generateExpectedApp("ksonnet-guestbook"),
}
var expectedAppsNewNamespace []argov1alpha1.Application
var expectedAppsNewMetadata []argov1alpha1.Application
Given(t).
When().
// Create a GitGenerator-based ApplicationSet
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-git-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{path.basename}}"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "{{path}}",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "{{path.basename}}",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
Directories: []v1alpha1.GitDirectoryGeneratorItem{
{
Path: "*guestbook*",
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist(expectedApps)).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
And(func() {
for _, expectedApp := range expectedApps {
newExpectedApp := expectedApp.DeepCopy()
newExpectedApp.Spec.Destination.Namespace = "guestbook2"
expectedAppsNewNamespace = append(expectedAppsNewNamespace, *newExpectedApp)
}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
}).Then().Expect(ApplicationsExist(expectedAppsNewNamespace)).
// Update the metadata fields in the appset template, and make sure it propagates to the apps
When().
And(func() {
for _, expectedApp := range expectedAppsNewNamespace {
expectedAppNewMetadata := expectedApp.DeepCopy()
expectedAppNewMetadata.ObjectMeta.Annotations = map[string]string{"annotation-key": "annotation-value"}
expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{"label-key": "label-value"}
expectedAppsNewMetadata = append(expectedAppsNewMetadata, *expectedAppNewMetadata)
}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Annotations = map[string]string{"annotation-key": "annotation-value"}
appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
}).Then().Expect(ApplicationsExist(expectedAppsNewMetadata)).
// verify the ApplicationSet status conditions were set correctly
Expect(ApplicationSetHasConditions("simple-git-generator", ExpectedConditions)).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
}
func TestSimpleGitFilesGenerator(t *testing.T) {
generateExpectedApp := func(name string) argov1alpha1.Application {
return argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}
}
expectedApps := []argov1alpha1.Application{
generateExpectedApp("engineering-dev-guestbook"),
generateExpectedApp("engineering-prod-guestbook"),
}
var expectedAppsNewNamespace []argov1alpha1.Application
var expectedAppsNewMetadata []argov1alpha1.Application
Given(t).
When().
// Create a GitGenerator-based ApplicationSet
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-git-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster.name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/applicationset.git",
Files: []v1alpha1.GitFileGeneratorItem{
{
Path: "examples/git-generator-files-discovery/cluster-config/**/config.json",
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist(expectedApps)).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
And(func() {
for _, expectedApp := range expectedApps {
newExpectedApp := expectedApp.DeepCopy()
newExpectedApp.Spec.Destination.Namespace = "guestbook2"
expectedAppsNewNamespace = append(expectedAppsNewNamespace, *newExpectedApp)
}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
}).Then().Expect(ApplicationsExist(expectedAppsNewNamespace)).
// Update the metadata fields in the appset template, and make sure it propagates to the apps
When().
And(func() {
for _, expectedApp := range expectedAppsNewNamespace {
expectedAppNewMetadata := expectedApp.DeepCopy()
expectedAppNewMetadata.ObjectMeta.Annotations = map[string]string{"annotation-key": "annotation-value"}
expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{"label-key": "label-value"}
expectedAppsNewMetadata = append(expectedAppsNewMetadata, *expectedAppNewMetadata)
}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Annotations = map[string]string{"annotation-key": "annotation-value"}
appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
}).Then().Expect(ApplicationsExist(expectedAppsNewMetadata)).
// verify the ApplicationSet status conditions were set correctly
Expect(ApplicationSetHasConditions("simple-git-generator", ExpectedConditions)).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
}
func TestSimpleGitFilesPreserveResourcesOnDeletion(t *testing.T) {
Given(t).
When().
CreateNamespace().
// Create a GitGenerator-based ApplicationSet
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-git-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster.name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: utils.ApplicationSetNamespace,
},
// Automatically create resources
SyncPolicy: &argov1alpha1.SyncPolicy{
Automated: &argov1alpha1.SyncPolicyAutomated{},
},
},
},
SyncPolicy: &v1alpha1.ApplicationSetSyncPolicy{
PreserveResourcesOnDeletion: true,
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
Git: &v1alpha1.GitGenerator{
RepoURL: "https://github.com/argoproj/applicationset.git",
Files: []v1alpha1.GitFileGeneratorItem{
{
Path: "examples/git-generator-files-discovery/cluster-config/**/config.json",
},
},
},
},
},
},
// We use an extra-long duration here, as we might need to wait for image pull.
}).Then().ExpectWithDuration(Pod(func(p corev1.Pod) bool { return strings.Contains(p.Name, "guestbook-ui") }), 6*time.Minute).
When().
Delete().
And(func() {
t.Log("Waiting 30 seconds to give the cluster a chance to delete the pods.")
// Wait 30 seconds to give the cluster a chance to deletes the pods, if it is going to do so.
// It should NOT delete the pods; to do so would be an ApplicationSet bug, and
// that is what we are testing here.
time.Sleep(30 * time.Second)
// The pod should continue to exist after 30 seconds.
}).Then().Expect(Pod(func(p corev1.Pod) bool { return strings.Contains(p.Name, "guestbook-ui") }))
}
func TestSimpleSCMProviderGenerator(t *testing.T) {
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-example-apps-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "git@github.com:argoproj/argocd-example-apps.git",
TargetRevision: "master",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}
// Because you can't &"".
repoMatch := "example-apps"
Given(t).
// Create an SCMProviderGenerator-based ApplicationSet
When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-scm-provider-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{ repository }}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "{{ url }}",
TargetRevision: "{{ branch }}",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
SCMProvider: &v1alpha1.SCMProviderGenerator{
Github: &v1alpha1.SCMProviderGeneratorGithub{
Organization: "argoproj",
},
Filters: []v1alpha1.SCMProviderGeneratorFilter{
{
RepositoryMatch: &repoMatch,
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp}))
}
func TestCustomApplicationFinalizers(t *testing.T) {
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io/background"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}
Given(t).
// Create a ListGenerator-based ApplicationSet
When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-list-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "{{cluster}}-guestbook",
Finalizers: []string{"resources-finalizer.argocd.argoproj.io/background"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "{{url}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{
Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
}},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedApp}))
}
func TestSimplePullRequestGenerator(t *testing.T) {
if utils.IsGitHubAPISkippedTest(t) {
return
}
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "guestbook-1",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "git@github.com:applicationset-test-org/argocd-example-apps.git",
TargetRevision: "824a5c987fdfb2b0629e9dbf5f31636c69ba4772",
Path: "kustomize-guestbook",
Kustomize: &argov1alpha1.ApplicationSourceKustomize{
NamePrefix: "guestbook-1",
},
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook-pull-request",
},
},
}
Given(t).
// Create an PullRequestGenerator-based ApplicationSet
When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-pull-request-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "guestbook-{{ number }}"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "git@github.com:applicationset-test-org/argocd-example-apps.git",
TargetRevision: "{{ head_sha }}",
Path: "kustomize-guestbook",
Kustomize: &argov1alpha1.ApplicationSourceKustomize{
NamePrefix: "guestbook-{{ number }}",
},
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook-{{ branch }}",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
PullRequest: &v1alpha1.PullRequestGenerator{
Github: &v1alpha1.PullRequestGeneratorGithub{
Owner: "applicationset-test-org",
Repo: "argocd-example-apps",
Labels: []string{
"preview",
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp}))
}

View file

@ -0,0 +1,389 @@
package e2e
import (
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
func TestSimpleClusterGenerator(t *testing.T) {
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "cluster1",
Namespace: "guestbook",
},
},
}
var expectedAppNewNamespace *argov1alpha1.Application
var expectedAppNewMetadata *argov1alpha1.Application
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
// Server: "{{server}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
Clusters: &v1alpha1.ClusterGenerator{
Selector: metav1.LabelSelector{
MatchLabels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
And(func() {
expectedAppNewNamespace = expectedApp.DeepCopy()
expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
// Update the metadata fields in the appset template, and make sure it propagates to the apps
When().
And(func() {
expectedAppNewMetadata = expectedAppNewNamespace.DeepCopy()
expectedAppNewMetadata.ObjectMeta.Annotations = map[string]string{"annotation-key": "annotation-value"}
expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{"label-key": "label-value"}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Annotations = map[string]string{"annotation-key": "annotation-value"}
appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewMetadata})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewNamespace}))
}
func TestClusterGeneratorWithLocalCluster(t *testing.T) {
expectedAppTemplate := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "in-cluster-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
// Destination comes from appDestination below
},
}
tests := []struct {
name string
appsetDestination argov1alpha1.ApplicationDestination
appDestination argov1alpha1.ApplicationDestination
}{
{
name: "specify local cluster by server field",
appDestination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
appsetDestination: argov1alpha1.ApplicationDestination{
Server: "{{server}}",
Namespace: "guestbook",
},
},
{
name: "specify local cluster by name field",
appDestination: argov1alpha1.ApplicationDestination{
Name: "in-cluster",
Namespace: "guestbook",
},
appsetDestination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
Namespace: "guestbook",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var expectedAppNewNamespace *argov1alpha1.Application
var expectedAppNewMetadata *argov1alpha1.Application
// Create the expected application from the template, and copy in the destination from the test case
expectedApp := *expectedAppTemplate.DeepCopy()
expectedApp.Spec.Destination = test.appDestination
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "in-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: test.appsetDestination,
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
Clusters: &v1alpha1.ClusterGenerator{},
},
},
},
}).Then().ExpectWithDuration(ApplicationsExist([]argov1alpha1.Application{expectedApp}), 8*time.Minute).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
And(func() {
expectedAppNewNamespace = expectedApp.DeepCopy()
expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
// Update the metadata fields in the appset template, and make sure it propagates to the apps
When().
And(func() {
expectedAppNewMetadata = expectedAppNewNamespace.DeepCopy()
expectedAppNewMetadata.ObjectMeta.Annotations = map[string]string{"annotation-key": "annotation-value"}
expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{"label-key": "label-value"}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Annotations = map[string]string{"annotation-key": "annotation-value"}
appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewMetadata})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewNamespace}))
})
}
}
func TestSimpleClusterGeneratorAddingCluster(t *testing.T) {
expectedAppTemplate := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "{{name}}-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
Namespace: "guestbook",
},
},
}
expectedAppCluster1 := *expectedAppTemplate.DeepCopy()
expectedAppCluster1.Spec.Destination.Name = "cluster1"
expectedAppCluster1.ObjectMeta.Name = "cluster1-guestbook"
expectedAppCluster2 := *expectedAppTemplate.DeepCopy()
expectedAppCluster2.Spec.Destination.Name = "cluster2"
expectedAppCluster2.ObjectMeta.Name = "cluster2-guestbook"
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
// Server: "{{server}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
Clusters: &v1alpha1.ClusterGenerator{
Selector: metav1.LabelSelector{
MatchLabels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
CreateClusterSecret("my-secret2", "cluster2", "https://kubernetes.default.svc").
Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1, expectedAppCluster2})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster1, expectedAppCluster2}))
}
func TestSimpleClusterGeneratorDeletingCluster(t *testing.T) {
expectedAppTemplate := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "{{name}}-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
Namespace: "guestbook",
},
},
}
expectedAppCluster1 := *expectedAppTemplate.DeepCopy()
expectedAppCluster1.Spec.Destination.Name = "cluster1"
expectedAppCluster1.ObjectMeta.Name = "cluster1-guestbook"
expectedAppCluster2 := *expectedAppTemplate.DeepCopy()
expectedAppCluster2.Spec.Destination.Name = "cluster2"
expectedAppCluster2.ObjectMeta.Name = "cluster2-guestbook"
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
CreateClusterSecret("my-secret2", "cluster2", "https://kubernetes.default.svc").
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
// Server: "{{server}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
Clusters: &v1alpha1.ClusterGenerator{
Selector: metav1.LabelSelector{
MatchLabels: map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1, expectedAppCluster2})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
DeleteClusterSecret("my-secret2").
Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1})).
Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster2})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster1}))
}

View file

@ -0,0 +1,412 @@
package e2e
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
var tenSec = int64(10)
func TestSimpleClusterDecisionResourceGenerator(t *testing.T) {
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "cluster1",
Namespace: "guestbook",
},
},
}
var expectedAppNewNamespace *argov1alpha1.Application
var expectedAppNewMetadata *argov1alpha1.Application
clusterList := []interface{}{
map[string]interface{}{
"clusterName": "cluster1",
"reason": "argotest",
},
}
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
CreatePlacementRoleAndRoleBinding().
CreatePlacementDecisionConfigMap("my-configmap").
CreatePlacementDecision("my-placementdecision").
StatusUpdatePlacementDecision("my-placementdecision", clusterList).
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{clusterName}}",
// Server: "{{server}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
ClusterDecisionResource: &v1alpha1.DuckTypeGenerator{
ConfigMapRef: "my-configmap",
Name: "my-placementdecision",
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
And(func() {
expectedAppNewNamespace = expectedApp.DeepCopy()
expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
// Update the metadata fields in the appset template, and make sure it propagates to the apps
When().
And(func() {
expectedAppNewMetadata = expectedAppNewNamespace.DeepCopy()
expectedAppNewMetadata.ObjectMeta.Annotations = map[string]string{"annotation-key": "annotation-value"}
expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{"label-key": "label-value"}
}).
Update(func(appset *v1alpha1.ApplicationSet) {
appset.Spec.Template.Annotations = map[string]string{"annotation-key": "annotation-value"}
appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewMetadata})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewNamespace}))
}
func TestSimpleClusterDecisionResourceGeneratorAddingCluster(t *testing.T) {
expectedAppTemplate := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "{{name}}-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{clusterName}}",
Namespace: "guestbook",
},
},
}
expectedAppCluster1 := *expectedAppTemplate.DeepCopy()
expectedAppCluster1.Spec.Destination.Name = "cluster1"
expectedAppCluster1.ObjectMeta.Name = "cluster1-guestbook"
expectedAppCluster2 := *expectedAppTemplate.DeepCopy()
expectedAppCluster2.Spec.Destination.Name = "cluster2"
expectedAppCluster2.ObjectMeta.Name = "cluster2-guestbook"
clusterList := []interface{}{
map[string]interface{}{
"clusterName": "cluster1",
"reason": "argotest",
},
map[string]interface{}{
"clusterName": "cluster2",
"reason": "argotest",
},
}
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
CreatePlacementRoleAndRoleBinding().
CreatePlacementDecisionConfigMap("my-configmap").
CreatePlacementDecision("my-placementdecision").
StatusUpdatePlacementDecision("my-placementdecision", clusterList).
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{clusterName}}",
// Server: "{{server}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
ClusterDecisionResource: &v1alpha1.DuckTypeGenerator{
ConfigMapRef: "my-configmap",
Name: "my-placementdecision",
RequeueAfterSeconds: &tenSec,
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
CreateClusterSecret("my-secret2", "cluster2", "https://kubernetes.default.svc").
Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1, expectedAppCluster2})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster1, expectedAppCluster2}))
}
func TestSimpleClusterDecisionResourceGeneratorDeletingClusterSecret(t *testing.T) {
expectedAppTemplate := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "{{name}}-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
Namespace: "guestbook",
},
},
}
expectedAppCluster1 := *expectedAppTemplate.DeepCopy()
expectedAppCluster1.Spec.Destination.Name = "cluster1"
expectedAppCluster1.ObjectMeta.Name = "cluster1-guestbook"
expectedAppCluster2 := *expectedAppTemplate.DeepCopy()
expectedAppCluster2.Spec.Destination.Name = "cluster2"
expectedAppCluster2.ObjectMeta.Name = "cluster2-guestbook"
clusterList := []interface{}{
map[string]interface{}{
"clusterName": "cluster1",
"reason": "argotest",
},
map[string]interface{}{
"clusterName": "cluster2",
"reason": "argotest",
},
}
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
CreateClusterSecret("my-secret2", "cluster2", "https://kubernetes.default.svc").
CreatePlacementRoleAndRoleBinding().
CreatePlacementDecisionConfigMap("my-configmap").
CreatePlacementDecision("my-placementdecision").
StatusUpdatePlacementDecision("my-placementdecision", clusterList).
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{clusterName}}",
// Server: "{{server}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
ClusterDecisionResource: &v1alpha1.DuckTypeGenerator{
ConfigMapRef: "my-configmap",
Name: "my-placementdecision",
RequeueAfterSeconds: &tenSec,
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1, expectedAppCluster2})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
DeleteClusterSecret("my-secret2").
Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1})).
Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster2})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster1}))
}
func TestSimpleClusterDecisionResourceGeneratorDeletingClusterFromResource(t *testing.T) {
expectedAppTemplate := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "{{name}}-guestbook",
Namespace: utils.ArgoCDNamespace,
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{name}}",
Namespace: "guestbook",
},
},
}
expectedAppCluster1 := *expectedAppTemplate.DeepCopy()
expectedAppCluster1.Spec.Destination.Name = "cluster1"
expectedAppCluster1.ObjectMeta.Name = "cluster1-guestbook"
expectedAppCluster2 := *expectedAppTemplate.DeepCopy()
expectedAppCluster2.Spec.Destination.Name = "cluster2"
expectedAppCluster2.ObjectMeta.Name = "cluster2-guestbook"
clusterList := []interface{}{
map[string]interface{}{
"clusterName": "cluster1",
"reason": "argotest",
},
map[string]interface{}{
"clusterName": "cluster2",
"reason": "argotest",
},
}
clusterListSmall := []interface{}{
map[string]interface{}{
"clusterName": "cluster1",
"reason": "argotest",
},
}
Given(t).
// Create a ClusterGenerator-based ApplicationSet
When().
CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
CreateClusterSecret("my-secret2", "cluster2", "https://kubernetes.default.svc").
CreatePlacementRoleAndRoleBinding().
CreatePlacementDecisionConfigMap("my-configmap").
CreatePlacementDecision("my-placementdecision").
StatusUpdatePlacementDecision("my-placementdecision", clusterList).
Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "simple-cluster-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Name: "{{clusterName}}",
// Server: "{{server}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
ClusterDecisionResource: &v1alpha1.DuckTypeGenerator{
ConfigMapRef: "my-configmap",
Name: "my-placementdecision",
RequeueAfterSeconds: &tenSec,
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1, expectedAppCluster2})).
// Update the ApplicationSet template namespace, and verify it updates the Applications
When().
StatusUpdatePlacementDecision("my-placementdecision", clusterListSmall).
Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedAppCluster1})).
Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster2})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedAppCluster1}))
}

View file

@ -0,0 +1,483 @@
package applicationsets
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
argocommon "github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
// this implements the "when" part of given/when/then
//
// none of the func implement error checks, and that is complete intended, you should check for errors
// using the Then()
type Actions struct {
context *Context
lastOutput string
lastError error
describeAction string
ignoreErrors bool
}
var pdGVR = schema.GroupVersionResource{
Group: "cluster.open-cluster-management.io",
Version: "v1alpha1",
Resource: "placementdecisions",
}
// IgnoreErrors sets whether to ignore
func (a *Actions) IgnoreErrors() *Actions {
a.ignoreErrors = true
return a
}
func (a *Actions) DoNotIgnoreErrors() *Actions {
a.ignoreErrors = false
return a
}
func (a *Actions) And(block func()) *Actions {
a.context.t.Helper()
block()
return a
}
func (a *Actions) Then() *Consequences {
a.context.t.Helper()
return &Consequences{a.context, a}
}
// GetServiceAccountBearerToken will attempt to get the provided service account until it
// exists, iterate the secrets associated with it looking for one of type
// kubernetes.io/service-account-token, and return it's token if found.
// (function based on 'GetServiceAccountBearerToken' from Argo CD's 'clusterauth.go')
func GetServiceAccountBearerToken(clientset kubernetes.Interface, ns string, sa string) (string, error) {
var serviceAccount *corev1.ServiceAccount
var secret *corev1.Secret
var err error
err = wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) {
serviceAccount, err = clientset.CoreV1().ServiceAccounts(ns).Get(context.Background(), sa, metav1.GetOptions{})
if err != nil {
return false, err
}
// Scan all secrets looking for one of the correct type:
for _, oRef := range serviceAccount.Secrets {
var getErr error
secret, err = clientset.CoreV1().Secrets(ns).Get(context.Background(), oRef.Name, metav1.GetOptions{})
if err != nil {
return false, fmt.Errorf("failed to retrieve secret %q: %v", oRef.Name, getErr)
}
if secret.Type == corev1.SecretTypeServiceAccountToken {
return true, nil
}
}
return false, nil
})
if err != nil {
return "", fmt.Errorf("failed to wait for service account secret: %v", err)
}
token, ok := secret.Data["token"]
if !ok {
return "", fmt.Errorf("secret %q for service account %q did not have a token", secret.Name, serviceAccount)
}
return string(token), nil
}
// CreateClusterSecret creates a faux cluster secret, with the given cluster server and cluster name (this cluster
// will not actually be used by the Argo CD controller, but that's not needed for our E2E tests)
func (a *Actions) CreateClusterSecret(secretName string, clusterName string, clusterServer string) *Actions {
fixtureClient := utils.GetE2EFixtureK8sClient()
var serviceAccountName string
// Look for a service account matching '*application-controller*'
err := wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) {
serviceAccountList, err := fixtureClient.KubeClientset.CoreV1().ServiceAccounts(utils.ArgoCDNamespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
fmt.Println("Unable to retrieve ServiceAccount list", err)
return false, nil
}
// If 'application-controller' service account is present, use that
for _, sa := range serviceAccountList.Items {
if strings.Contains(sa.Name, "application-controller") {
serviceAccountName = sa.Name
return true, nil
}
}
// Otherwise, use 'default'
for _, sa := range serviceAccountList.Items {
if sa.Name == "default" {
serviceAccountName = sa.Name
return true, nil
}
}
return false, nil
})
if err == nil {
var bearerToken string
bearerToken, err = GetServiceAccountBearerToken(fixtureClient.KubeClientset, utils.ArgoCDNamespace, serviceAccountName)
// bearerToken
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: utils.ArgoCDNamespace,
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
utils.TestingLabel: "true",
},
},
Data: map[string][]byte{
"name": []byte(clusterName),
"server": []byte(clusterServer),
"config": []byte("{\"username\":\"foo\",\"password\":\"foo\"}"),
},
}
// If the bearer token is available, use it rather than the fake username/password
if bearerToken != "" && err == nil {
secret.Data = map[string][]byte{
"name": []byte(clusterName),
"server": []byte(clusterServer),
"config": []byte("{\"bearerToken\":\"" + bearerToken + "\"}"),
}
}
_, err = fixtureClient.KubeClientset.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{})
}
a.describeAction = fmt.Sprintf("creating cluster Secret '%s'", secretName)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// DeleteClusterSecret deletes a faux cluster secret
func (a *Actions) DeleteClusterSecret(secretName string) *Actions {
err := utils.GetE2EFixtureK8sClient().KubeClientset.CoreV1().Secrets(utils.ArgoCDNamespace).Delete(context.Background(), secretName, metav1.DeleteOptions{})
a.describeAction = fmt.Sprintf("deleting cluster Secret '%s'", secretName)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// DeleteConfigMap deletes a faux cluster secret
func (a *Actions) DeleteConfigMap(configMapName string) *Actions {
err := utils.GetE2EFixtureK8sClient().KubeClientset.CoreV1().ConfigMaps(utils.ArgoCDNamespace).Delete(context.Background(), configMapName, metav1.DeleteOptions{})
a.describeAction = fmt.Sprintf("deleting configMap '%s'", configMapName)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// DeletePlacementDecision deletes a faux cluster secret
func (a *Actions) DeletePlacementDecision(placementDecisionName string) *Actions {
err := utils.GetE2EFixtureK8sClient().DynamicClientset.Resource(pdGVR).Namespace(utils.ArgoCDNamespace).Delete(context.Background(), placementDecisionName, metav1.DeleteOptions{})
a.describeAction = fmt.Sprintf("deleting placement decision '%s'", placementDecisionName)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// Create a temporary namespace, from utils.ApplicationSet, for use by the test.
// This namespace will be deleted on subsequent tests.
func (a *Actions) CreateNamespace() *Actions {
a.context.t.Helper()
fixtureClient := utils.GetE2EFixtureK8sClient()
_, err := fixtureClient.KubeClientset.CoreV1().Namespaces().Create(context.Background(),
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: utils.ApplicationSetNamespace}}, metav1.CreateOptions{})
a.describeAction = fmt.Sprintf("creating namespace '%s'", utils.ApplicationSetNamespace)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// Create creates an ApplicationSet using the provided value
func (a *Actions) Create(appSet v1alpha1.ApplicationSet) *Actions {
a.context.t.Helper()
appSet.APIVersion = "argoproj.io/v1alpha1"
appSet.Kind = "ApplicationSet"
fixtureClient := utils.GetE2EFixtureK8sClient()
newResource, err := fixtureClient.AppSetClientset.Create(context.Background(), utils.MustToUnstructured(&appSet), metav1.CreateOptions{})
if err == nil {
a.context.name = newResource.GetName()
}
a.describeAction = fmt.Sprintf("creating ApplicationSet '%s'", appSet.Name)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// Create Role/RoleBinding to allow ApplicationSet to list the PlacementDecisions
func (a *Actions) CreatePlacementRoleAndRoleBinding() *Actions {
fixtureClient := utils.GetE2EFixtureK8sClient()
var err error
_, err = fixtureClient.KubeClientset.RbacV1().Roles(utils.ArgoCDNamespace).Create(context.Background(), &v1.Role{
ObjectMeta: metav1.ObjectMeta{Name: "placement-role", Namespace: utils.ArgoCDNamespace},
Rules: []v1.PolicyRule{
{
Verbs: []string{"get", "list", "watch"},
APIGroups: []string{"cluster.open-cluster-management.io"},
Resources: []string{"placementdecisions"},
},
},
}, metav1.CreateOptions{})
if err != nil && strings.Contains(err.Error(), "already exists") {
err = nil
}
if err == nil {
_, err = fixtureClient.KubeClientset.RbacV1().RoleBindings(utils.ArgoCDNamespace).Create(context.Background(),
&v1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{Name: "placement-role-binding", Namespace: utils.ArgoCDNamespace},
Subjects: []v1.Subject{
{
Name: "argocd-applicationset-controller",
Namespace: utils.ArgoCDNamespace,
Kind: "ServiceAccount",
},
},
RoleRef: v1.RoleRef{
Kind: "Role",
APIGroup: "rbac.authorization.k8s.io",
Name: "placement-role",
},
}, metav1.CreateOptions{})
}
if err != nil && strings.Contains(err.Error(), "already exists") {
err = nil
}
a.describeAction = "creating placement role/rolebinding"
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// Create a ConfigMap for the ClusterResourceList generator
func (a *Actions) CreatePlacementDecisionConfigMap(configMapName string) *Actions {
a.context.t.Helper()
fixtureClient := utils.GetE2EFixtureK8sClient()
_, err := fixtureClient.KubeClientset.CoreV1().ConfigMaps(utils.ArgoCDNamespace).Get(context.Background(), configMapName, metav1.GetOptions{})
// Don't do anything if it exists
if err == nil {
return a
}
_, err = fixtureClient.KubeClientset.CoreV1().ConfigMaps(utils.ArgoCDNamespace).Create(context.Background(),
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
},
Data: map[string]string{
"apiVersion": "cluster.open-cluster-management.io/v1alpha1",
"kind": "placementdecisions",
"statusListKey": "decisions",
"matchKey": "clusterName",
},
}, metav1.CreateOptions{})
a.describeAction = fmt.Sprintf("creating configmap '%s'", configMapName)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
func (a *Actions) CreatePlacementDecision(placementDecisionName string) *Actions {
a.context.t.Helper()
fixtureClient := utils.GetE2EFixtureK8sClient().DynamicClientset
_, err := fixtureClient.Resource(pdGVR).Namespace(utils.ArgoCDNamespace).Get(
context.Background(),
placementDecisionName,
metav1.GetOptions{})
// If already exists
if err == nil {
return a
}
placementDecision := &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": placementDecisionName,
"namespace": utils.ArgoCDNamespace,
},
"kind": "PlacementDecision",
"apiVersion": "cluster.open-cluster-management.io/v1alpha1",
"status": map[string]interface{}{},
},
}
_, err = fixtureClient.Resource(pdGVR).Namespace(utils.ArgoCDNamespace).Create(
context.Background(),
placementDecision,
metav1.CreateOptions{})
a.describeAction = fmt.Sprintf("creating placementDecision '%v'", placementDecisionName)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
func (a *Actions) StatusUpdatePlacementDecision(placementDecisionName string, clusterList []interface{}) *Actions {
a.context.t.Helper()
fixtureClient := utils.GetE2EFixtureK8sClient().DynamicClientset
placementDecision, err := fixtureClient.Resource(pdGVR).Namespace(utils.ArgoCDNamespace).Get(
context.Background(),
placementDecisionName,
metav1.GetOptions{})
placementDecision.Object["status"] = map[string]interface{}{
"decisions": clusterList,
}
if err == nil {
_, err = fixtureClient.Resource(pdGVR).Namespace(utils.ArgoCDNamespace).UpdateStatus(
context.Background(),
placementDecision,
metav1.UpdateOptions{})
}
a.describeAction = fmt.Sprintf("status update placementDecision for '%v'", clusterList)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// Delete deletes the ApplicationSet within the context
func (a *Actions) Delete() *Actions {
a.context.t.Helper()
fixtureClient := utils.GetE2EFixtureK8sClient()
deleteProp := metav1.DeletePropagationForeground
err := fixtureClient.AppSetClientset.Delete(context.Background(), a.context.name, metav1.DeleteOptions{PropagationPolicy: &deleteProp})
a.describeAction = fmt.Sprintf("Deleting ApplicationSet '%s' %v", a.context.name, err)
a.lastOutput, a.lastError = "", err
a.verifyAction()
return a
}
// get retrieves the ApplicationSet (by name) that was created by an earlier Create action
func (a *Actions) get() (*v1alpha1.ApplicationSet, error) {
appSet := v1alpha1.ApplicationSet{}
fixtureClient := utils.GetE2EFixtureK8sClient()
newResource, err := fixtureClient.AppSetClientset.Get(context.Background(), a.context.name, metav1.GetOptions{})
if err != nil {
return nil, err
}
bytes, err := newResource.MarshalJSON()
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, &appSet)
if err != nil {
return nil, err
}
return &appSet, nil
}
// Update retrieves the latest copy the ApplicationSet, then allows the caller to mutate it via 'toUpdate', with
// the result applied back to the cluster resource
func (a *Actions) Update(toUpdate func(*v1alpha1.ApplicationSet)) *Actions {
a.context.t.Helper()
timeout := 30 * time.Second
var mostRecentError error
for start := time.Now(); time.Since(start) < timeout; time.Sleep(3 * time.Second) {
appSet, err := a.get()
mostRecentError = err
if err == nil {
// Keep trying to update until it succeeds, or the test times out
toUpdate(appSet)
a.describeAction = fmt.Sprintf("updating ApplicationSet '%s'", appSet.Name)
fixtureClient := utils.GetE2EFixtureK8sClient()
_, err = fixtureClient.AppSetClientset.Update(context.Background(), utils.MustToUnstructured(&appSet), metav1.UpdateOptions{})
if err != nil {
mostRecentError = err
} else {
mostRecentError = nil
break
}
}
}
a.lastOutput, a.lastError = "", mostRecentError
a.verifyAction()
return a
}
func (a *Actions) verifyAction() {
a.context.t.Helper()
if a.describeAction != "" {
log.Infof("action: %s", a.describeAction)
a.describeAction = ""
}
if !a.ignoreErrors {
a.Then().Expect(Success(""))
}
}

View file

@ -0,0 +1,111 @@
package applicationsets
import (
"context"
"encoding/json"
"time"
"github.com/argoproj/pkg/errors"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
// this implements the "then" part of given/when/then
type Consequences struct {
context *Context
actions *Actions
}
func (c *Consequences) Expect(e Expectation) *Consequences {
return c.ExpectWithDuration(e, time.Duration(30)*time.Second)
}
func (c *Consequences) ExpectWithDuration(e Expectation, timeout time.Duration) *Consequences {
// this invocation makes sure this func is not reported as the cause of the failure - we are a "test helper"
c.context.t.Helper()
var message string
var state state
for start := time.Now(); time.Since(start) < timeout; time.Sleep(3 * time.Second) {
state, message = e(c)
switch state {
case succeeded:
log.Infof("expectation succeeded: %s", message)
return c
case failed:
c.context.t.Fatalf("failed expectation: %s", message)
return c
}
log.Infof("expectation pending: %s", message)
}
c.context.t.Fatal("timeout waiting for: " + message)
return c
}
func (c *Consequences) And(block func()) *Consequences {
c.context.t.Helper()
block()
return c
}
func (c *Consequences) Given() *Context {
return c.context
}
func (c *Consequences) When() *Actions {
return c.actions
}
func (c *Consequences) app(name string) *argov1alpha1.Application {
apps := c.apps()
for index, app := range apps {
if app.Name == name {
return &apps[index]
}
}
return nil
}
func (c *Consequences) apps() []argov1alpha1.Application {
fixtureClient := utils.GetE2EFixtureK8sClient()
list, err := fixtureClient.AppClientset.ArgoprojV1alpha1().Applications(utils.ArgoCDNamespace).List(context.Background(), metav1.ListOptions{})
errors.CheckError(err)
if list == nil {
list = &argov1alpha1.ApplicationList{}
}
return list.Items
}
func (c *Consequences) applicationSet(applicationSetName string) *v1alpha1.ApplicationSet {
fixtureClient := utils.GetE2EFixtureK8sClient()
list, err := fixtureClient.AppSetClientset.Get(context.Background(), c.actions.context.name, metav1.GetOptions{})
errors.CheckError(err)
var appSet v1alpha1.ApplicationSet
bytes, err := list.MarshalJSON()
if err != nil {
return &v1alpha1.ApplicationSet{}
}
err = json.Unmarshal(bytes, &appSet)
if err != nil {
return &v1alpha1.ApplicationSet{}
}
if appSet.Name == applicationSetName {
return &appSet
}
return &v1alpha1.ApplicationSet{}
}

View file

@ -0,0 +1,37 @@
package applicationsets
import (
"testing"
"time"
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
// Context implements the "given" part of given/when/then
type Context struct {
t *testing.T
// name is the ApplicationSet's name, created by a Create action
name string
}
func Given(t *testing.T) *Context {
EnsureCleanState(t)
return &Context{t: t}
}
func (c *Context) When() *Actions {
// in case any settings have changed, pause for 1s, not great, but fine
time.Sleep(1 * time.Second)
return &Actions{context: c}
}
func (c *Context) Sleep(seconds time.Duration) *Context {
time.Sleep(seconds * time.Second)
return c
}
func (c *Context) And(block func()) *Context {
block()
return c
}

View file

@ -0,0 +1,237 @@
package applicationsets
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/argoproj/gitops-engine/pkg/diff"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils"
)
type state = string
const (
failed = "failed"
pending = "pending"
succeeded = "succeeded"
)
// Expectation returns succeeded on succes condition, or pending/failed on failure, along with
// a message to describe the success/failure condition.
type Expectation func(c *Consequences) (state state, message string)
// Success asserts that the last command was successful
func Success(message string) Expectation {
return func(c *Consequences) (state, string) {
if c.actions.lastError != nil {
return failed, fmt.Sprintf("error: %v", c.actions.lastError)
}
if !strings.Contains(c.actions.lastOutput, message) {
return failed, fmt.Sprintf("output did not contain '%s'", message)
}
return succeeded, fmt.Sprintf("no error and output contained '%s'", message)
}
}
// Error asserts that the last command was an error with substring match
func Error(message, err string) Expectation {
return func(c *Consequences) (state, string) {
if c.actions.lastError == nil {
return failed, "no error"
}
if !strings.Contains(c.actions.lastOutput, message) {
return failed, fmt.Sprintf("output does not contain '%s'", message)
}
if !strings.Contains(c.actions.lastError.Error(), err) {
return failed, fmt.Sprintf("error does not contain '%s'", message)
}
return succeeded, fmt.Sprintf("error '%s'", message)
}
}
// ApplicationsExist checks whether each of the 'expectedApps' exist in the namespace, and are
// equivalent to provided values.
func ApplicationsExist(expectedApps []argov1alpha1.Application) Expectation {
return func(c *Consequences) (state, string) {
for _, expectedApp := range expectedApps {
foundApp := c.app(expectedApp.Name)
if foundApp == nil {
return pending, fmt.Sprintf("missing app '%s'", expectedApp.Name)
}
if !appsAreEqual(expectedApp, *foundApp) {
diff, err := getDiff(filterFields(expectedApp), filterFields(*foundApp))
if err != nil {
return failed, err.Error()
}
return pending, fmt.Sprintf("apps are not equal: '%s', diff: %s\n", expectedApp.Name, diff)
}
}
return succeeded, "all apps successfully found"
}
}
// ApplicationSetHasConditions checks whether each of the 'expectedConditions' exist in the ApplicationSet status, and are
// equivalent to provided values.
func ApplicationSetHasConditions(applicationSetName string, expectedConditions []v1alpha1.ApplicationSetCondition) Expectation {
return func(c *Consequences) (state, string) {
// retrieve the application set
foundApplicationSet := c.applicationSet(applicationSetName)
if foundApplicationSet == nil {
return pending, fmt.Sprintf("application set '%s' not found", applicationSetName)
}
if !conditionsAreEqual(&expectedConditions, &foundApplicationSet.Status.Conditions) {
diff, err := getConditionDiff(expectedConditions, foundApplicationSet.Status.Conditions)
if err != nil {
return failed, err.Error()
}
return pending, fmt.Sprintf("application set conditions are not equal: '%s', diff: %s\n", expectedConditions, diff)
}
return succeeded, "application set successfully found"
}
}
// ApplicationsDoNotExist checks that each of the 'expectedApps' no longer exist in the namespace
func ApplicationsDoNotExist(expectedApps []argov1alpha1.Application) Expectation {
return func(c *Consequences) (state, string) {
for _, expectedApp := range expectedApps {
foundApp := c.app(expectedApp.Name)
if foundApp != nil {
return pending, fmt.Sprintf("app '%s' should no longer exist", expectedApp.Name)
}
}
return succeeded, "all apps do not exist"
}
}
// Pod checks whether a specified condition is true for any of the pods in the namespace
func Pod(predicate func(p corev1.Pod) bool) Expectation {
return func(c *Consequences) (state, string) {
pods, err := pods(utils.ApplicationSetNamespace)
if err != nil {
return failed, err.Error()
}
for _, pod := range pods.Items {
if predicate(pod) {
return succeeded, fmt.Sprintf("pod predicate matched pod named '%s'", pod.GetName())
}
}
return pending, "pod predicate does not match pods"
}
}
func pods(namespace string) (*corev1.PodList, error) {
fixtureClient := utils.GetE2EFixtureK8sClient()
pods, err := fixtureClient.KubeClientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
return pods, err
}
// getDiff returns a string containing a comparison result of two applications (for test output/debug purposes)
func getDiff(orig, new argov1alpha1.Application) (string, error) {
bytes, _, err := diff.CreateTwoWayMergePatch(orig, new, orig)
if err != nil {
return "", err
}
return string(bytes), nil
}
// getConditionDiff returns a string containing a comparison result of two ApplicationSetCondition (for test output/debug purposes)
func getConditionDiff(orig, new []v1alpha1.ApplicationSetCondition) (string, error) {
if len(orig) != len(new) {
return fmt.Sprintf("mismatch between condition sizes: %v %v", len(orig), len(new)), nil
}
var bytes []byte
for index := range orig {
b, _, err := diff.CreateTwoWayMergePatch(orig[index], new[index], orig[index])
if err != nil {
return "", err
}
bytes = append(bytes, b...)
}
return string(bytes), nil
}
// filterFields returns a copy of Application, but with unnecessary (for testing) fields removed
func filterFields(input argov1alpha1.Application) argov1alpha1.Application {
spec := input.Spec
metaCopy := input.ObjectMeta.DeepCopy()
output := argov1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Labels: metaCopy.Labels,
Annotations: metaCopy.Annotations,
Name: metaCopy.Name,
Namespace: metaCopy.Namespace,
Finalizers: metaCopy.Finalizers,
},
Spec: argov1alpha1.ApplicationSpec{
Source: argov1alpha1.ApplicationSource{
Path: spec.Source.Path,
RepoURL: spec.Source.RepoURL,
TargetRevision: spec.Source.TargetRevision,
},
Destination: argov1alpha1.ApplicationDestination{
Server: spec.Destination.Server,
Name: spec.Destination.Name,
Namespace: spec.Destination.Namespace,
},
Project: spec.Project,
},
}
return output
}
// filterConditionFields returns a copy of ApplicationSetCondition, but with unnecessary (for testing) fields removed
func filterConditionFields(input *[]v1alpha1.ApplicationSetCondition) *[]v1alpha1.ApplicationSetCondition {
var filteredConditions []v1alpha1.ApplicationSetCondition
for _, condition := range *input {
newCondition := &v1alpha1.ApplicationSetCondition{
Type: condition.Type,
Status: condition.Status,
Message: condition.Message,
Reason: condition.Reason,
}
filteredConditions = append(filteredConditions, *newCondition)
}
return &filteredConditions
}
// appsAreEqual returns true if the apps are equal, comparing only fields of interest
func appsAreEqual(one argov1alpha1.Application, two argov1alpha1.Application) bool {
return reflect.DeepEqual(filterFields(one), filterFields(two))
}
// conditionsAreEqual returns true if the appset status conditions are equal, comparing only fields of interest
func conditionsAreEqual(one, two *[]v1alpha1.ApplicationSetCondition) bool {
return reflect.DeepEqual(filterConditionFields(one), filterConditionFields(two))
}

View file

@ -0,0 +1,24 @@
package utils
import (
"os"
"os/exec"
"strings"
argoexec "github.com/argoproj/pkg/exec"
)
func Run(workDir, name string, args ...string) (string, error) {
return RunWithStdin("", workDir, name, args...)
}
func RunWithStdin(stdin, workDir, name string, args ...string) (string, error) {
cmd := exec.Command(name, args...)
if stdin != "" {
cmd.Stdin = strings.NewReader(stdin)
}
cmd.Env = os.Environ()
cmd.Dir = workDir
return argoexec.RunCommandExt(cmd, argoexec.CmdOpts{})
}

Some files were not shown because too many files have changed in this diff Show more