pkg/argocd/argoapplicationset_manager.go (397 lines of code) (raw):
package argocd
import (
"context"
"encoding/json"
"fmt"
argoApi "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"golang.org/x/exp/maps"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
cdPipeApi "github.com/epam/edp-cd-pipeline-operator/v2/api/v1"
codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
)
type generatorElement struct {
Stage string `json:"stage"`
Codebase string `json:"codebase"`
ImageTag string `json:"imageTag"`
ImageRepository string `json:"imageRepository"`
Cluster string `json:"cluster"`
Namespace string `json:"namespace"`
RepoURL string `json:"repoURL"`
GitUrlPath string `json:"gitUrlPath"`
VersionType string `json:"versionType"`
CustomValues bool `json:"customValues"`
}
const codebaseTypeSystem = "system"
var gitOpsCodebaseLabels = map[string]string{
"app.edp.epam.com/codebaseType": "system",
"app.edp.epam.com/systemType": "gitops",
}
type ArgoApplicationSetManager struct {
client client.Client
}
func NewArgoApplicationSetManager(k8sClient client.Client) *ArgoApplicationSetManager {
return &ArgoApplicationSetManager{client: k8sClient}
}
func (c *ArgoApplicationSetManager) CreateApplicationSet(ctx context.Context, pipeline *cdPipeApi.CDPipeline) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Creating ArgoApplicationSet")
appset := &argoApi.ApplicationSet{}
err := c.client.Get(ctx, client.ObjectKey{
Namespace: pipeline.Namespace,
Name: pipeline.Name,
}, appset)
if client.IgnoreNotFound(err) != nil {
return fmt.Errorf("failed to get ArgoApplicationSet: %w", err)
}
if err == nil {
log.Info("ArgoApplicationSet already exists. Skip creating")
return nil
}
if len(pipeline.Spec.Applications) == 0 {
log.Info("No applications specified. Skip creating ArgoApplicationSet")
return nil
}
gitopsUrl, err := c.getGitOpsRepoUrl(ctx, pipeline.Namespace)
if err != nil {
return err
}
appset = generateApplicationSet(pipeline, gitopsUrl)
if err = controllerutil.SetOwnerReference(pipeline, appset, c.client.Scheme()); err != nil {
return fmt.Errorf("failed to set ApplicationSet owner reference: %w", err)
}
if err = c.client.Create(ctx, appset); err != nil {
return fmt.Errorf("failed to create ArgoApplicationSet: %w", err)
}
log.Info("ArgoApplicationSet has been created")
return nil
}
func (c *ArgoApplicationSetManager) CreateApplicationSetGenerators(ctx context.Context, stage *cdPipeApi.Stage) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Creating ArgoApplicationSetGenerator")
pipeline := &cdPipeApi.CDPipeline{}
if err := c.client.Get(ctx, client.ObjectKey{
Namespace: stage.Namespace,
Name: stage.Spec.CdPipeline,
}, pipeline); err != nil {
return fmt.Errorf("failed to get CDPipeline: %w", err)
}
appset := &argoApi.ApplicationSet{}
if err := c.client.Get(ctx, client.ObjectKey{
Namespace: stage.Namespace,
Name: pipeline.Name,
}, appset); err != nil {
return fmt.Errorf("failed to get ArgoApplicationSet: %w", err)
}
codebases, err := c.getPipelinesCodebasesMap(ctx, stage.Namespace, pipeline.Spec.Applications)
if err != nil {
return err
}
gitServers, err := c.getGitServers(ctx, stage.Namespace, codebases)
if err != nil {
return err
}
stageGenerators, err := c.makeStageGenerators(ctx, stage, codebases, gitServers)
if err != nil {
return err
}
changed, err := setGenerators(stage.Spec.Name, appset, stageGenerators)
if err != nil {
return err
}
if changed {
if err = c.client.Update(ctx, appset); err != nil {
return fmt.Errorf("failed to update ArgoApplicationSet: %w", err)
}
log.Info("ArgoApplicationSet has been updated")
return nil
}
log.Info("ArgoApplicationSet generators are already set")
return nil
}
func (c *ArgoApplicationSetManager) RemoveApplicationSetGenerators(ctx context.Context, stage *cdPipeApi.Stage) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Removing ArgoApplicationSetGenerator")
appset := &argoApi.ApplicationSet{}
if err := c.client.Get(ctx, client.ObjectKey{
Namespace: stage.Namespace,
Name: stage.Labels[cdPipeApi.StageCdPipelineLabelName],
}, appset); err != nil {
if errors.IsNotFound(err) {
log.Info("ArgoApplicationSet is not found. Skip removing generators")
return nil
}
return fmt.Errorf("failed to get ArgoApplicationSet: %w", err)
}
for i := 0; i < len(appset.Spec.Generators); i++ {
if appset.Spec.Generators[i].List == nil {
continue
}
n := 0
for _, rawel := range appset.Spec.Generators[i].List.Elements {
el := &generatorElement{}
if err := json.Unmarshal(rawel.Raw, el); err != nil {
return fmt.Errorf("failed to unmarshal generator element: %w", err)
}
if el.Stage != stage.Spec.Name {
appset.Spec.Generators[i].List.Elements[n] = rawel
n++
}
}
if len(appset.Spec.Generators[i].List.Elements) != n {
appset.Spec.Generators[i].List.Elements = appset.Spec.Generators[i].List.Elements[:n]
if err := c.client.Update(ctx, appset); err != nil {
return fmt.Errorf("failed to update ArgoApplicationSet: %w", err)
}
log.Info("Stage generator was removed from ArgoApplicationSet")
return nil
}
break
}
log.Info("Stage generators are not found in ArgoApplicationSet")
return nil
}
func (c *ArgoApplicationSetManager) makeStageGenerators(
ctx context.Context,
stage *cdPipeApi.Stage,
codebases map[string]codebaseApi.Codebase,
gitServers map[string]codebaseApi.GitServer,
) (map[string]apiextensionsv1.JSON, error) {
stageGenerators := make(map[string]apiextensionsv1.JSON, len(codebases))
for k := range codebases {
spec := codebases[k].Spec
image, err := c.getImageRepo(ctx, codebases[k].Namespace, codebases[k].Name, spec.DefaultBranch)
if err != nil {
return nil, err
}
gitServer, ok := gitServers[spec.GitServer]
if !ok {
return nil, fmt.Errorf("git server %s not found", spec.GitServer)
}
gen := generatorElement{
Stage: stage.Spec.Name,
Codebase: codebases[k].Name,
ImageTag: "NaN",
ImageRepository: image,
Cluster: stage.Spec.ClusterName,
Namespace: stage.Spec.Namespace,
RepoURL: fmt.Sprintf(
"ssh://%s@%s:%d%s",
gitServer.Spec.GitUser,
gitServer.Spec.GitHost,
gitServer.Spec.SshPort,
spec.GitUrlPath,
),
GitUrlPath: spec.GetProjectID(),
VersionType: string(spec.Versioning.Type),
CustomValues: false,
}
var raw []byte
if raw, err = json.Marshal(gen); err != nil {
return nil, fmt.Errorf("failed to marshal generator element: %w", err)
}
stageGenerators[fmt.Sprintf("%s-%s", codebases[k].Name, stage.Spec.Name)] = apiextensionsv1.JSON{Raw: raw}
}
return stageGenerators, nil
}
func (c *ArgoApplicationSetManager) getImageRepo(ctx context.Context, ns, codebaseName, branch string) (string, error) {
image := &codebaseApi.CodebaseImageStream{}
if err := c.client.Get(ctx, client.ObjectKey{
Namespace: ns,
Name: fmt.Sprintf("%s-%s", codebaseName, branch),
}, image); err != nil {
return "", fmt.Errorf("failed to get CodebaseImageStream: %w", err)
}
return image.Spec.ImageName, nil
}
// TODO: we can optimize this method by getting all codebases at once. We need to add label with name to codebase.
func (c *ArgoApplicationSetManager) getPipelinesCodebasesMap(ctx context.Context, ns string, apps []string) (map[string]codebaseApi.Codebase, error) {
m := make(map[string]codebaseApi.Codebase, len(apps))
for _, app := range apps {
codebase := &codebaseApi.Codebase{}
if err := c.client.Get(ctx, client.ObjectKey{
Namespace: ns,
Name: app,
}, codebase); err != nil {
return nil, fmt.Errorf("failed to get Codebase: %w", err)
}
m[app] = *codebase
}
return m, nil
}
func (c *ArgoApplicationSetManager) getGitServers(
ctx context.Context,
ns string,
codebases map[string]codebaseApi.Codebase,
) (map[string]codebaseApi.GitServer, error) {
gitServerNames := make(map[string]struct{}, len(codebases))
for k := range codebases {
gitServerNames[codebases[k].Spec.GitServer] = struct{}{}
}
gitServers := make(map[string]codebaseApi.GitServer, len(gitServerNames))
for gitServerName := range gitServerNames {
gitServer := &codebaseApi.GitServer{}
if err := c.client.Get(ctx, client.ObjectKey{
Namespace: ns,
Name: gitServerName,
}, gitServer); err != nil {
return nil, fmt.Errorf("failed to get GitServer: %w", err)
}
gitServers[gitServer.Name] = *gitServer
}
return gitServers, nil
}
func (c *ArgoApplicationSetManager) getGitOpsRepoUrl(ctx context.Context, ns string) (string, error) {
codebaseList := &codebaseApi.CodebaseList{}
if err := c.client.List(ctx, codebaseList, client.InNamespace(ns), client.MatchingLabels(gitOpsCodebaseLabels)); err != nil {
return "", fmt.Errorf("failed to list codebases: %w", err)
}
if len(codebaseList.Items) == 0 {
return "", fmt.Errorf("no GitOps codebases found")
}
if len(codebaseList.Items) > 1 {
return "", fmt.Errorf("found more than one GitOps codebase")
}
gitOpsCodebase := &codebaseList.Items[0]
if gitOpsCodebase.Spec.Type != codebaseTypeSystem {
return "", fmt.Errorf("gitOps codebase does not have %q type", codebaseTypeSystem)
}
gitServer := &codebaseApi.GitServer{}
if err := c.client.Get(ctx, client.ObjectKey{
Namespace: ns,
Name: gitOpsCodebase.Spec.GitServer,
}, gitServer); err != nil {
return "", fmt.Errorf("failed to get gitops GitServer: %w", err)
}
return fmt.Sprintf(
"ssh://%s@%s:%d%s",
gitServer.Spec.GitUser,
gitServer.Spec.GitHost,
gitServer.Spec.SshPort,
gitOpsCodebase.Spec.GitUrlPath,
), nil
}
func generateTemplatePatch(pipeline, gitopsUrl string) string {
template := `
{{- if .customValues }}
spec:
sources:
- ref: values
RepoURL: %s
targetRevision: main
- helm:
parameters:
- name: image.tag
value: '{{ .imageTag }}'
- name: image.repository
value: {{ .imageRepository }}
releaseName: '{{ .codebase }}'
valueFiles:
- $values/%s/{{ .stage }}/{{ .codebase }}-values.yaml
path: deploy-templates
RepoURL: {{ .repoURL }}
targetRevision: '{{ if eq .versionType "edp" }}build/{{ .imageTag }}{{ else }}{{ .imageTag }}{{ end }}'
{{- end }}`
return fmt.Sprintf(template, gitopsUrl, pipeline)
}
func generateApplicationSet(
pipeline *cdPipeApi.CDPipeline,
gitopsUrl string,
) *argoApi.ApplicationSet {
templatePatch := generateTemplatePatch(pipeline.Name, gitopsUrl)
return &argoApi.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: pipeline.Name,
Namespace: pipeline.Namespace,
},
Spec: argoApi.ApplicationSetSpec{
Generators: []argoApi.ApplicationSetGenerator{},
GoTemplate: true,
GoTemplateOptions: []string{"missingkey=error"},
TemplatePatch: &templatePatch,
Template: argoApi.ApplicationSetTemplate{
ApplicationSetTemplateMeta: argoApi.ApplicationSetTemplateMeta{
Name: fmt.Sprintf("%s-{{ .stage }}-{{ .codebase }}", pipeline.Name),
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"}, // check if it is our or argo's responsibility
Labels: map[string]string{
"app.edp.epam.com/app-name": "{{ .codebase }}",
"app.edp.epam.com/pipeline": pipeline.Name,
"app.edp.epam.com/stage": "{{ .stage }}",
},
},
Spec: argoApi.ApplicationSpec{
Destination: argoApi.ApplicationDestination{
Name: "{{ .cluster }}",
Namespace: "{{ .namespace }}",
},
Project: pipeline.Namespace,
Source: &argoApi.ApplicationSource{
Helm: &argoApi.ApplicationSourceHelm{
Parameters: []argoApi.HelmParameter{
{
Name: "image.tag",
Value: "{{ .imageTag }}",
},
{
Name: "image.repository",
Value: "{{ .imageRepository }}",
},
},
ReleaseName: "{{ .codebase }}",
},
Path: "deploy-templates",
RepoURL: "{{ .repoURL }}",
TargetRevision: `{{ if eq .versionType "edp" }}build/{{ .imageTag }}{{ else }}{{ .imageTag }}{{ end }}`,
},
},
},
},
}
}
func setGenerators(stageName string, appset *argoApi.ApplicationSet, stageGenerators map[string]apiextensionsv1.JSON) (bool, error) {
if len(appset.Spec.Generators) == 0 {
appset.Spec.Generators = []argoApi.ApplicationSetGenerator{
{
List: &argoApi.ListGenerator{},
},
}
}
for i := 0; i < len(appset.Spec.Generators); i++ {
if appset.Spec.Generators[i].List == nil {
continue
}
changed, err := processGeneratorListElements(stageName, &appset.Spec.Generators[i], stageGenerators)
if err != nil {
return false, err
}
return changed, nil
}
return false, nil
}
func processGeneratorListElements(stageName string, generator *argoApi.ApplicationSetGenerator, stageGenerators map[string]apiextensionsv1.JSON) (bool, error) {
n := 0
for _, rawel := range generator.List.Elements {
el := &generatorElement{}
if err := json.Unmarshal(rawel.Raw, el); err != nil {
return false, fmt.Errorf("failed to unmarshal generator element: %w", err)
}
key := fmt.Sprintf("%s-%s", el.Codebase, el.Stage)
_, ok := stageGenerators[key]
if ok {
delete(stageGenerators, key)
}
if el.Stage != stageName || (el.Stage == stageName && ok) {
generator.List.Elements[n] = rawel
n++
}
}
if len(generator.List.Elements) != n || len(stageGenerators) > 0 {
generator.List.Elements = generator.List.Elements[:n]
generator.List.Elements = append(generator.List.Elements, maps.Values(stageGenerators)...)
return true, nil
}
return false, nil
}