diff --git a/main.go b/main.go index eb4ef3b..871f176 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,12 @@ func main() { "Additional facts added to the dynamic facts in the cluster object. Keys in the configmap's data field can't override existing keys."). Default("additional-facts"). StringVar(&agent.AdditionalFactsConfigMap) + app. + Flag( + "additional-root-apps-config-map", + "Config map holding metadata for additional ArgoCD root apps and app projects."). + Default("additional-root-apps"). + StringVar(&agent.AdditionalRootAppsConfigMap) app. Flag( "ocp-oauth-route-namespace", diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 31a5661..1ec26fa 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -36,6 +36,9 @@ type Agent struct { // The configmap containing additional facts to be added to the dynamic facts AdditionalFactsConfigMap string + // The configmap containing metadata for additional root apps to deploy + AdditionalRootAppsConfigMap string + // Reference to the OpenShift OAuth route to be added to the dynamic facts OCPOAuthRouteNamespace string OCPOAuthRouteName string @@ -140,7 +143,7 @@ func (a *Agent) registerCluster(ctx context.Context, config *rest.Config, apiCli return } - if err := argocd.Apply(ctx, config, a.Namespace, a.OperatorNamespace, a.ArgoCDImage, a.RedisImage, apiClient, cluster); err != nil { + if err := argocd.Apply(ctx, config, a.Namespace, a.OperatorNamespace, a.ArgoCDImage, a.RedisImage, a.AdditionalRootAppsConfigMap, apiClient, cluster); err != nil { klog.Error(err) } } diff --git a/pkg/argocd/argo-app.go b/pkg/argocd/argo-app.go index a173f40..d120481 100644 --- a/pkg/argocd/argo-app.go +++ b/pkg/argocd/argo-app.go @@ -2,13 +2,17 @@ package argocd import ( "context" + "encoding/json" + "fmt" "github.com/projectsyn/lieutenant-api/pkg/api" + "k8s.io/apimachinery/pkg/api/errors" k8err "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/klog" ) @@ -23,20 +27,48 @@ var ( argoProjectGVR = argoGroupVersion.WithResource("appprojects") localKubernetesAPI = "https://kubernetes.default.svc" + + additionalRootAppsConfigKey = "teams" ) -func createArgoProject(ctx context.Context, cluster *api.Cluster, config *rest.Config, namespace string) error { +func readAdditionalRootAppsConfigMap(ctx context.Context, clientset *kubernetes.Clientset, namespace, additionalRootAppsConfigMapName string) ([]string, error) { + cm, err := clientset.CoreV1().ConfigMaps(namespace).Get(ctx, additionalRootAppsConfigMapName, v1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + klog.Info("Additional root apps config map not present") + return []string{}, nil + } else { + return nil, fmt.Errorf("unable to fetch the additional root apps config map: %w", err) + } + } + teamsJson, ok := cm.Data[additionalRootAppsConfigKey] + if !ok { + return nil, fmt.Errorf("additional root apps ConfigMap doesn't have key %s", additionalRootAppsConfigKey) + } + var teams []string + if err := json.Unmarshal([]byte(teamsJson), &teams); err != nil { + return nil, fmt.Errorf("unmarshalling additional root apps ConfigMap contents: %v", err) + } + return teams, nil +} + +func createArgoProject(ctx context.Context, cluster *api.Cluster, config *rest.Config, namespace, name string) error { dynamicClient, err := dynamic.NewForConfig(config) if err != nil { return err } argoProjectClient := dynamicClient.Resource(argoProjectGVR) + + if _, err = argoProjectClient.Namespace(namespace).Get(ctx, name, v1.GetOptions{}); err == nil { + return nil + } + project := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": argoProjectGVR.Group + "/" + argoProjectGVR.Version, "kind": "AppProject", "metadata": map[string]interface{}{ - "name": argoProjectName, + "name": name, }, "spec": map[string]interface{}{ "clusterResourceWhitelist": []map[string]interface{}{{ @@ -56,39 +88,44 @@ func createArgoProject(ctx context.Context, cluster *api.Cluster, config *rest.C if _, err = argoProjectClient.Namespace(namespace).Create(ctx, project, v1.CreateOptions{}); err != nil { if k8err.IsAlreadyExists(err) { - klog.Warning("Argo Project already exists, skip") + klog.Warning("Argo Project already exists, skipping... app=", name) } else { return err } } else { - klog.Info("Argo Project created") + klog.Info("Argo Project created: ", name) } return nil } -func createArgoApp(ctx context.Context, cluster *api.Cluster, config *rest.Config, namespace string) error { +func createArgoApp(ctx context.Context, cluster *api.Cluster, config *rest.Config, namespace, projectName, name, appsPath string) error { dynamicClient, err := dynamic.NewForConfig(config) if err != nil { return err } argoAppClient := dynamicClient.Resource(argoAppGVR) + + if _, err = argoAppClient.Namespace(namespace).Get(ctx, name, v1.GetOptions{}); err == nil { + return nil + } + app := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": argoAppGVR.Group + "/" + argoAppGVR.Version, "kind": "Application", "metadata": map[string]interface{}{ - "name": argoRootAppName, + "name": name, }, "spec": map[string]interface{}{ - "project": argoProjectName, + "project": projectName, "source": map[string]interface{}{ "repoURL": *cluster.GitRepo.Url, - "path": argoAppsPath, + "path": appsPath + "/", "targetRevision": "HEAD", }, "syncPolicy": map[string]interface{}{ "automated": map[string]interface{}{ - "prune": true, + "prune": false, "selfHeal": true, }, }, @@ -102,12 +139,12 @@ func createArgoApp(ctx context.Context, cluster *api.Cluster, config *rest.Confi if _, err = argoAppClient.Namespace(namespace).Create(ctx, app, v1.CreateOptions{}); err != nil { if k8err.IsAlreadyExists(err) { - klog.Warning("Argo App already exists, skip") + klog.Warning("Argo App already exists, skipping... app=", name) } else { return err } } else { - klog.Info("Argo App created") + klog.Info("Argo App created: ", name) } return nil } diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index 7c2e52d..e6f793f 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -26,23 +26,23 @@ var ( argoAnnotations = map[string]string{ "argocd.argoproj.io/sync-options": "Prune=false", } - argoSSHSecretName = "argo-ssh-key" - argoSSHPublicKey = "sshPublicKey" - argoSSHPrivateKey = "sshPrivateKey" - argoSSHConfigMapName = "argocd-ssh-known-hosts-cm" - argoTLSConfigMapName = "argocd-tls-certs-cm" - argoRbacConfigMapName = "argocd-rbac-cm" - argoConfigMapName = "argocd-cm" - argoSecretName = "argocd-secret" - argoClusterSecretName = "syn-argocd-cluster" - argoRbacName = "argocd-application-controller" - argoRootAppName = "root" - argoProjectName = "syn" - argoAppsPath = "manifests/apps/" + argoSSHSecretName = "argo-ssh-key" + argoSSHPublicKey = "sshPublicKey" + argoSSHPrivateKey = "sshPrivateKey" + argoSSHConfigMapName = "argocd-ssh-known-hosts-cm" + argoTLSConfigMapName = "argocd-tls-certs-cm" + argoRbacConfigMapName = "argocd-rbac-cm" + argoConfigMapName = "argocd-cm" + argoSecretName = "argocd-secret" + argoClusterSecretName = "syn-argocd-cluster" + argoRbacName = "argocd-application-controller" + defaultArgoRootAppName = "root" + defaultArgoProjectName = "syn" + argoAppsPathPrefix = "manifests/apps" ) // Apply reconciles the Argo CD deployments -func Apply(ctx context.Context, config *rest.Config, namespace, operatorNamespace, argoImage, redisArgoImage string, apiClient *api.Client, cluster *api.Cluster) error { +func Apply(ctx context.Context, config *rest.Config, namespace, operatorNamespace, argoImage, redisArgoImage, additionalRootAppsConfigMapName string, apiClient *api.Client, cluster *api.Cluster) error { clientset, err := kubernetes.NewForConfig(config) if err != nil { return err @@ -65,6 +65,10 @@ func Apply(ctx context.Context, config *rest.Config, namespace, operatorNamespac return err } + if err = applyAdditionalRootApps(ctx, clientset, config, namespace, additionalRootAppsConfigMapName, cluster); err != nil { + return err + } + if err == nil && len(argos.Items) > 0 { // An ArgoCD custom resource exists in our namespace err = fixArgoOperatorDeadlock(ctx, clientset, config, namespace, operatorNamespace) @@ -122,11 +126,11 @@ func bootstrapArgo(ctx context.Context, clientset *kubernetes.Clientset, config return err } - if err := createArgoProject(ctx, cluster, config, namespace); err != nil { + if err := createArgoProject(ctx, cluster, config, namespace, defaultArgoProjectName); err != nil { return err } - if err := createArgoApp(ctx, cluster, config, namespace); err != nil { + if err := createArgoApp(ctx, cluster, config, namespace, defaultArgoProjectName, defaultArgoRootAppName, argoAppsPathPrefix); err != nil { return err } @@ -189,3 +193,23 @@ func fixArgoOperatorDeadlock(ctx context.Context, clientset *kubernetes.Clientse return multierr.Combine(errors...) } + +func applyAdditionalRootApps(ctx context.Context, clientset *kubernetes.Clientset, config *rest.Config, namespace, additionalRootAppsConfigMapName string, cluster *api.Cluster) error { + teamNames, err := readAdditionalRootAppsConfigMap(ctx, clientset, namespace, additionalRootAppsConfigMapName) + if err != nil { + return err + } + + for _, name := range teamNames { + if err := createArgoProject(ctx, cluster, config, namespace, name); err != nil { + return err + } + + // apps path for additional root apps is `manifests/apps-/`. + if err := createArgoApp(ctx, cluster, config, namespace, name, "root-"+name, argoAppsPathPrefix+"-"+name); err != nil { + return err + } + } + + return nil +}