mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
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:
parent
8847a310ad
commit
c77cf66aa1
104 changed files with 23270 additions and 14429 deletions
5
.github/workflows/ci-build.yaml
vendored
5
.github/workflows/ci-build.yaml
vendored
|
|
@ -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() }}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
Makefile
5
Makefile
|
|
@ -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
|
||||
1
Procfile
1
Procfile
|
|
@ -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}"
|
||||
|
|
|
|||
724
applicationset/controllers/applicationset_controller.go
Normal file
724
applicationset/controllers/applicationset_controller.go
Normal 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(), ¤t, 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{}
|
||||
1927
applicationset/controllers/applicationset_controller_test.go
Normal file
1927
applicationset/controllers/applicationset_controller_test.go
Normal file
File diff suppressed because it is too large
Load diff
84
applicationset/controllers/clustereventhandler.go
Normal file
84
applicationset/controllers/clustereventhandler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
234
applicationset/controllers/clustereventhandler_test.go
Normal file
234
applicationset/controllers/clustereventhandler_test.go
Normal 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
|
||||
}
|
||||
184
applicationset/generators/cluster.go
Normal file
184
applicationset/generators/cluster.go
Normal 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, "-.")
|
||||
}
|
||||
254
applicationset/generators/cluster_test.go
Normal file
254
applicationset/generators/cluster_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
229
applicationset/generators/duck_type.go
Normal file
229
applicationset/generators/duck_type.go
Normal 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
|
||||
}
|
||||
315
applicationset/generators/duck_type_test.go
Normal file
315
applicationset/generators/duck_type_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
83
applicationset/generators/generator_spec_processor.go
Normal file
83
applicationset/generators/generator_spec_processor.go
Normal 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
|
||||
}
|
||||
225
applicationset/generators/git.go
Normal file
225
applicationset/generators/git.go
Normal 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
|
||||
}
|
||||
452
applicationset/generators/git_test.go
Normal file
452
applicationset/generators/git_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
32
applicationset/generators/interface.go
Normal file
32
applicationset/generators/interface.go
Normal 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
|
||||
)
|
||||
74
applicationset/generators/list.go
Normal file
74
applicationset/generators/list.go
Normal 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
|
||||
}
|
||||
39
applicationset/generators/list_test.go
Normal file
39
applicationset/generators/list_test.go
Normal 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)
|
||||
|
||||
}
|
||||
}
|
||||
157
applicationset/generators/matrix.go
Normal file
157
applicationset/generators/matrix.go
Normal 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
|
||||
}
|
||||
284
applicationset/generators/matrix_test.go
Normal file
284
applicationset/generators/matrix_test.go
Normal 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)
|
||||
|
||||
}
|
||||
215
applicationset/generators/merge.go
Normal file
215
applicationset/generators/merge.go
Normal 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
|
||||
}
|
||||
328
applicationset/generators/merge_test.go
Normal file
328
applicationset/generators/merge_test.go
Normal 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)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
114
applicationset/generators/pull_request.go
Normal file
114
applicationset/generators/pull_request.go
Normal 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
|
||||
}
|
||||
136
applicationset/generators/pull_request_test.go
Normal file
136
applicationset/generators/pull_request_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
124
applicationset/generators/scm_provider.go
Normal file
124
applicationset/generators/scm_provider.go
Normal 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
|
||||
}
|
||||
115
applicationset/generators/scm_provider_test.go
Normal file
115
applicationset/generators/scm_provider_test.go
Normal 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"])
|
||||
}
|
||||
23
applicationset/services/pull_request/fake.go
Normal file
23
applicationset/services/pull_request/fake.go
Normal 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
|
||||
}
|
||||
99
applicationset/services/pull_request/github.go
Normal file
99
applicationset/services/pull_request/github.go
Normal 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
|
||||
}
|
||||
59
applicationset/services/pull_request/github_test.go
Normal file
59
applicationset/services/pull_request/github_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
17
applicationset/services/pull_request/interface.go
Normal file
17
applicationset/services/pull_request/interface.go
Normal 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)
|
||||
}
|
||||
151
applicationset/services/repo_service.go
Normal file
151
applicationset/services/repo_service.go
Normal 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
|
||||
}
|
||||
233
applicationset/services/repo_service_test.go
Normal file
233
applicationset/services/repo_service_test.go
Normal 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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
177
applicationset/services/scm_provider/bitbucket_cloud.go
Normal file
177
applicationset/services/scm_provider/bitbucket_cloud.go
Normal 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)
|
||||
}
|
||||
511
applicationset/services/scm_provider/bitbucket_cloud_test.go
Normal file
511
applicationset/services/scm_provider/bitbucket_cloud_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
155
applicationset/services/scm_provider/github.go
Normal file
155
applicationset/services/scm_provider/github.go
Normal 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
|
||||
}
|
||||
117
applicationset/services/scm_provider/github_test.go
Normal file
117
applicationset/services/scm_provider/github_test.go
Normal 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)
|
||||
}
|
||||
151
applicationset/services/scm_provider/gitlab.go
Normal file
151
applicationset/services/scm_provider/gitlab.go
Normal 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
|
||||
}
|
||||
89
applicationset/services/scm_provider/gitlab_test.go
Normal file
89
applicationset/services/scm_provider/gitlab_test.go
Normal 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)
|
||||
}
|
||||
50
applicationset/services/scm_provider/mock.go
Normal file
50
applicationset/services/scm_provider/mock.go
Normal 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
|
||||
}
|
||||
42
applicationset/services/scm_provider/types.go
Normal file
42
applicationset/services/scm_provider/types.go
Normal 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
|
||||
)
|
||||
167
applicationset/services/scm_provider/utils.go
Normal file
167
applicationset/services/scm_provider/utils.go
Normal 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
|
||||
}
|
||||
292
applicationset/services/scm_provider/utils_test.go
Normal file
292
applicationset/services/scm_provider/utils_test.go
Normal 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: ®exp.Regexp{},
|
||||
FilterType: FilterTypeBranch,
|
||||
}
|
||||
repoFilter := Filter{
|
||||
RepositoryMatch: ®exp.Regexp{},
|
||||
FilterType: FilterTypeRepo,
|
||||
}
|
||||
pathExistsFilter := Filter{
|
||||
PathsExist: []string{"test"},
|
||||
FilterType: FilterTypeBranch,
|
||||
}
|
||||
labelMatchFilter := Filter{
|
||||
LabelMatch: ®exp.Regexp{},
|
||||
FilterType: FilterTypeRepo,
|
||||
}
|
||||
unsetFilter := Filter{
|
||||
LabelMatch: ®exp.Regexp{},
|
||||
}
|
||||
additionalBranchFilter := Filter{
|
||||
BranchMatch: ®exp.Regexp{},
|
||||
FilterType: FilterTypeBranch,
|
||||
}
|
||||
filterMap := getApplicableFilters([]*Filter{&branchFilter, &repoFilter,
|
||||
&pathExistsFilter, &labelMatchFilter, &unsetFilter, &additionalBranchFilter})
|
||||
|
||||
assert.Len(t, filterMap[FilterTypeRepo], 2)
|
||||
assert.Len(t, filterMap[FilterTypeBranch], 3)
|
||||
}
|
||||
196
applicationset/utils/clusterUtils.go
Normal file
196
applicationset/utils/clusterUtils.go
Normal 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
|
||||
}
|
||||
179
applicationset/utils/clusterUtils_test.go
Normal file
179
applicationset/utils/clusterUtils_test.go
Normal 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())
|
||||
})
|
||||
|
||||
}
|
||||
97
applicationset/utils/createOrUpdate.go
Normal file
97
applicationset/utils/createOrUpdate.go
Normal 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
|
||||
}
|
||||
38
applicationset/utils/map.go
Normal file
38
applicationset/utils/map.go
Normal 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
|
||||
}
|
||||
58
applicationset/utils/map_test.go
Normal file
58
applicationset/utils/map_test.go
Normal 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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
44
applicationset/utils/policy.go
Normal file
44
applicationset/utils/policy.go
Normal 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
|
||||
}
|
||||
186
applicationset/utils/testdata/github-commit-event.json
vendored
Normal file
186
applicationset/utils/testdata/github-commit-event.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
454
applicationset/utils/testdata/github-pull-request-assigned-event.json
vendored
Normal file
454
applicationset/utils/testdata/github-pull-request-assigned-event.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
454
applicationset/utils/testdata/github-pull-request-opened-event.json
vendored
Normal file
454
applicationset/utils/testdata/github-pull-request-opened-event.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
65
applicationset/utils/testdata/gitlab-event.json
vendored
Normal file
65
applicationset/utils/testdata/gitlab-event.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
3
applicationset/utils/testdata/invalid-event.json
vendored
Normal file
3
applicationset/utils/testdata/invalid-event.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"event":"invalid"
|
||||
}
|
||||
179
applicationset/utils/utils.go
Normal file
179
applicationset/utils/utils.go
Normal 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
|
||||
}
|
||||
}
|
||||
652
applicationset/utils/utils_test.go
Normal file
652
applicationset/utils/utils_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
299
applicationset/utils/webhook.go
Normal file
299
applicationset/utils/webhook.go
Normal 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)
|
||||
})
|
||||
}
|
||||
208
applicationset/utils/webhook_test.go
Normal file
208
applicationset/utils/webhook_test.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
10
go.mod
|
|
@ -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
23
go.sum
|
|
@ -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
13
hack/boilerplate.go.txt
Normal 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.
|
||||
*/
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- https://raw.githubusercontent.com/argoproj/applicationset/master/manifests/install.yaml
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -14,3 +14,4 @@ resources:
|
|||
- ./config
|
||||
- ./redis
|
||||
- ./notification
|
||||
- ./applicationset-controller
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ resources:
|
|||
- ../cluster-rbac/application-controller
|
||||
- ../base/config
|
||||
- ../base/application-controller
|
||||
- ../base/applicationset-controller
|
||||
- ../base/repo-server
|
||||
- ../base/redis
|
||||
images:
|
||||
|
|
|
|||
6163
manifests/crds/applicationset-crd.yaml
Normal file
6163
manifests/crds/applicationset-crd.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,3 +4,4 @@ kind: Kustomization
|
|||
resources:
|
||||
- application-crd.yaml
|
||||
- appproject-crd.yaml
|
||||
- applicationset-crd.yaml
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ images:
|
|||
newTag: latest
|
||||
resources:
|
||||
- ../../base/application-controller
|
||||
- ../../base/applicationset-controller
|
||||
- ../../base/dex
|
||||
- ../../base/repo-server
|
||||
- ../../base/server
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
502
pkg/apis/applicationset/v1alpha1/applicationset_types.go
Normal file
502
pkg/apis/applicationset/v1alpha1/applicationset_types.go
Normal 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
|
||||
}
|
||||
133
pkg/apis/applicationset/v1alpha1/applicationset_types_test.go
Normal file
133
pkg/apis/applicationset/v1alpha1/applicationset_types_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
36
pkg/apis/applicationset/v1alpha1/groupversion_info.go
Normal file
36
pkg/apis/applicationset/v1alpha1/groupversion_info.go
Normal 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
|
||||
)
|
||||
857
pkg/apis/applicationset/v1alpha1/zz_generated.deepcopy.go
Normal file
857
pkg/apis/applicationset/v1alpha1/zz_generated.deepcopy.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
616
test/e2e/applicationset_test.go
Normal file
616
test/e2e/applicationset_test.go
Normal 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}))
|
||||
}
|
||||
389
test/e2e/cluster_generator_test.go
Normal file
389
test/e2e/cluster_generator_test.go
Normal 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}))
|
||||
}
|
||||
412
test/e2e/clusterdecisiongenerator_e2e_test.go
Normal file
412
test/e2e/clusterdecisiongenerator_e2e_test.go
Normal 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}))
|
||||
}
|
||||
483
test/e2e/fixture/applicationsets/actions.go
Normal file
483
test/e2e/fixture/applicationsets/actions.go
Normal 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(""))
|
||||
}
|
||||
|
||||
}
|
||||
111
test/e2e/fixture/applicationsets/consequences.go
Normal file
111
test/e2e/fixture/applicationsets/consequences.go
Normal 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{}
|
||||
}
|
||||
37
test/e2e/fixture/applicationsets/context.go
Normal file
37
test/e2e/fixture/applicationsets/context.go
Normal 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
|
||||
}
|
||||
237
test/e2e/fixture/applicationsets/expectation.go
Normal file
237
test/e2e/fixture/applicationsets/expectation.go
Normal 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))
|
||||
}
|
||||
24
test/e2e/fixture/applicationsets/utils/cmd.go
Normal file
24
test/e2e/fixture/applicationsets/utils/cmd.go
Normal 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
Loading…
Reference in a new issue