diff --git a/cmd/deploy_flux.go b/cmd/deploy_flux.go index 4470dda..e9c75c9 100644 --- a/cmd/deploy_flux.go +++ b/cmd/deploy_flux.go @@ -3,41 +3,53 @@ package cmd import ( "fmt" - "github.com/openmcp-project/controller-utils/pkg/clusters" "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + cfg "github.com/openmcp-project/bootstrapper/internal/config" "github.com/openmcp-project/bootstrapper/internal/flux_deployer" logging "github.com/openmcp-project/bootstrapper/internal/log" -) - -const ( - flagOCMConfig = "ocm-config" - flagGitCredentials = "git-credentials" - flagKubeconfig = "kubeconfig" - flagFluxCDNamespace = "fluxcd-namespace" + "github.com/openmcp-project/bootstrapper/internal/util" ) // deployFluxCmd represents the "deploy flux" command var deployFluxCmd = &cobra.Command{ - Use: "deploy-flux source target", - Short: "Transfer an OCM component from a source to a target location", - Long: `Transfers the specified OCM component version from the source location to the target location.`, - Args: cobra.ExactArgs(5), + Use: "deploy-flux", + Short: "Deploys Flux controllers on the platform cluster, and establishes synchronization with a Git repository", + Long: `Deploys Flux controllers on the platform cluster, and establishes synchronization with a Git repository.`, + Args: cobra.ExactArgs(1), ArgAliases: []string{ - "component-location", - "deployment-templates", - "deployment-repository", - "deployment-repository-branch", - "deployment-repository-path", + "configFile", }, RunE: func(cmd *cobra.Command, args []string) error { + configFilePath := args[0] + log := logging.GetLogger() - log.Infof("Starting flux deployment with component-location: %s, deployment-templates: %s, "+ - "deployment-repository: %s, deployment-repository-branch: %s, deployment-repository-path: %s", - args[0], args[1], args[2], args[3], args[4]) + log.Infof("Starting deployment of Flux controllers with config file: %s.", configFilePath) - platformKubeconfig := cmd.Flag(flagKubeconfig).Value.String() - platformCluster := clusters.New("platform").WithConfigPath(platformKubeconfig) + // Configuration + config := &cfg.BootstrapperConfig{} + err := config.ReadFromFile(configFilePath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + config.SetDefaults() + err = config.Validate() + if err != nil { + return fmt.Errorf("invalid config file: %w", err) + } + + // Platform cluster + scheme := runtime.NewScheme() + if err := v1.AddToScheme(scheme); err != nil { + return fmt.Errorf("error adding corev1 to scheme: %w", err) + } + + platformCluster, err := util.GetCluster(cmd.Flag(FlagKubeConfig).Value.String(), "platform", scheme) + if err != nil { + return fmt.Errorf("failed to get platform cluster: %w", err) + } if err := platformCluster.InitializeRESTConfig(); err != nil { return fmt.Errorf("error initializing REST config for platform cluster: %w", err) } @@ -45,28 +57,25 @@ var deployFluxCmd = &cobra.Command{ return fmt.Errorf("error initializing client for platform cluster: %w", err) } - d := flux_deployer.NewFluxDeployer(args[0], args[1], args[2], args[3], args[4], - cmd.Flag(flagOCMConfig).Value.String(), - cmd.Flag(flagGitCredentials).Value.String(), - cmd.Flag(flagFluxCDNamespace).Value.String(), - platformKubeconfig, - platformCluster, log) - err := d.Deploy(cmd.Context()) - if err != nil { - log.Errorf("Flux deployment failed: %v", err) + d := flux_deployer.NewFluxDeployer(config, cmd.Flag(FlagGitConfig).Value.String(), cmd.Flag(FlagOcmConfig).Value.String(), platformCluster, log) + if err = d.Deploy(cmd.Context()); err != nil { + log.Errorf("Deployment of flux controllers failed: %v", err) return err } - log.Info("Flux deployment completed") + log.Info("Deployment of flux controllers completed") return nil }, } func init() { RootCmd.AddCommand(deployFluxCmd) + deployFluxCmd.Flags().SortFlags = false + deployFluxCmd.Flags().String(FlagOcmConfig, "", "OCM configuration file") + deployFluxCmd.Flags().String(FlagGitConfig, "", "Git credentials configuration file that configures basic auth or ssh private key. This will be used in the fluxcd GitSource for spec.secretRef to authenticate against the deploymentRepository. If not set, no authentication will be configured.") + deployFluxCmd.Flags().String(FlagKubeConfig, "", "Kubernetes configuration file") - deployFluxCmd.Flags().StringP(flagOCMConfig, "c", "", "ocm configuration file") - deployFluxCmd.Flags().StringP(flagGitCredentials, "g", "", "git credentials configuration file that configures basic auth, personal access token, ssh private key. This will be used in the fluxcd GitSource for spec.secretRef to authenticate against the deploymentRepository. If not set, no authentication will be configured.") - deployFluxCmd.Flags().StringP(flagKubeconfig, "k", "", "kubeconfig of the Kubernetes cluster on which the flux deployment will be created/updated. If not set, the current context will be used.") - deployFluxCmd.Flags().StringP(flagFluxCDNamespace, "n", "", "namespace on the Kubernetes cluster in which the namespaced fluxcd resources will be deployed. Default 'flux-system'.") + if err := deployFluxCmd.MarkFlagRequired(FlagGitConfig); err != nil { + panic(err) + } } diff --git a/docs/technical/flux-deployer.md b/docs/technical/flux-deployer.md new file mode 100644 index 0000000..5b35056 --- /dev/null +++ b/docs/technical/flux-deployer.md @@ -0,0 +1,50 @@ +# Flux Deployer + +The flux deployer creates a temporary working directory with subdirectories `download`, `templates`, and `repo`. + +The final directory structure looks like this: + +```shell +WORKDIR +├── download +│ ├── Chart.yaml +│ ├── templates +│ │ ├── overlays +│ │ │ ├── flux-kustomization.yaml +│ │ │ ├── gitrepo.yaml +│ │ │ └── kustomization.yaml +│ │ └── resources +│ │ ├── components.yaml +│ │ ├── flux-kustomization.yaml +│ │ ├── gitrepo.yaml +│ │ └── kustomization.yaml +│ └── values.yaml +│ +├── templates +│ ├── envs +│ │ └── dev +│ │ └── fluxcd +│ │ ├── flux-kustomization.yaml +│ │ ├── gitrepo.yaml +│ │ └── kustomization.yaml +│ └── resources +│ └── fluxcd +│ ├── components.yaml +│ ├── flux-kustomization.yaml +│ ├── gitrepo.yaml +│ └── kustomization.yaml +│ +└── repo # same structure as in WORKDIR/templates + ├── envs + │ └── dev + │ └── fluxcd # entry point for the kustomization + │ ├── flux-kustomization.yaml + │ ├── gitrepo.yaml + │ └── kustomization.yaml + └── resources + └── fluxcd + ├── components.yaml + ├── flux-kustomization.yaml + ├── gitrepo.yaml + └── kustomization.yaml +``` diff --git a/internal/flux_deployer/constants.go b/internal/flux_deployer/constants.go index 3d5f681..f4d575a 100644 --- a/internal/flux_deployer/constants.go +++ b/internal/flux_deployer/constants.go @@ -1,9 +1,23 @@ package flux_deployer const ( - // Names of ocm resources of the root component + // FluxSystemNamespace is the namespace on the platform cluster in which the flux controllers are deployed. + FluxSystemNamespace = "flux-system" - FluxcdHelmController = "fluxcd-helm-controller" - FluxcdKustomizeController = "fluxcd-kustomize-controller" - FluxcdSourceController = "fluxcd-source-controller" + // GitSecretName is the name of the secret in the flux system namespace that contains the git credentials for accessing the deployment repository. + // The secret is references in the GitRepository resource which establishes the synchronization with the deployment git repository. + GitSecretName = "git" + + // Directory names + EnvsDirectoryName = "envs" + FluxCDDirectoryName = "fluxcd" + OpenMCPDirectoryName = "openmcp" + ResourcesDirectoryName = "resources" + TemplatesDirectoryName = "templates" + OverlaysDirectoryName = "overlays" + + // Resource names + FluxCDSourceControllerResourceName = "fluxcd-source-controller" + FluxCDKustomizationControllerResourceName = "fluxcd-kustomize-controller" + FluxCDHelmControllerResourceName = "fluxcd-helm-controller" ) diff --git a/internal/flux_deployer/deployer.go b/internal/flux_deployer/deployer.go index d526276..797643f 100644 --- a/internal/flux_deployer/deployer.go +++ b/internal/flux_deployer/deployer.go @@ -5,166 +5,257 @@ import ( "fmt" "os" "path" + "path/filepath" "github.com/openmcp-project/controller-utils/pkg/clusters" + "github.com/openmcp-project/controller-utils/pkg/resources" "github.com/sirupsen/logrus" + "sigs.k8s.io/kustomize/api/krusty" + "sigs.k8s.io/kustomize/kyaml/filesys" + cfg "github.com/openmcp-project/bootstrapper/internal/config" ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" "github.com/openmcp-project/bootstrapper/internal/template" "github.com/openmcp-project/bootstrapper/internal/util" ) type FluxDeployer struct { - componentLocation string - - // deploymentTemplates is the path to the deployment templates in the templates component. - // It consists of segments separated by slashes. All segments except the last one are names of component references. - // The last segment is the name of a resource in the component, which is reached by starting at the root component and going through the component references. - // For example, "gitops-templates/fluxcd" means: start at the root component, go to the component reference "gitops-templates", and then use the resource named "fluxcd" in that component. - deploymentTemplates string - deploymentRepository string - deploymentRepositoryBranch string - deploymentRepositoryPath string - ocmConfig string - gitCredentials string - fluxcdNamespace string - platformKubeconfig string - platformCluster *clusters.Cluster - log *logrus.Logger -} + Config *cfg.BootstrapperConfig + + // GitConfigPath is the path to the Git configuration file + GitConfigPath string + // OcmConfigPath is the path to the OCM configuration file + OcmConfigPath string + + platformCluster *clusters.Cluster + fluxNamespace string + // fluxcdCV is the component version of the fluxcd source controller component + fluxcdCV *ocmcli.ComponentVersion + log *logrus.Logger -func NewFluxDeployer(componentLocation, deploymentTemplates, deploymentRepository, deploymentRepositoryBranch, deploymentRepositoryPath, - ocmConfig, gitCredentials, fluxcdNamespace, platformKubeconfig string, platformCluster *clusters.Cluster, log *logrus.Logger) *FluxDeployer { + workDir string + downloadDir string + templatesDir string + repoDir string +} +func NewFluxDeployer(config *cfg.BootstrapperConfig, gitConfigPath, ocmConfigPath string, platformCluster *clusters.Cluster, log *logrus.Logger) *FluxDeployer { return &FluxDeployer{ - componentLocation: componentLocation, - deploymentTemplates: deploymentTemplates, - deploymentRepository: deploymentRepository, - deploymentRepositoryBranch: deploymentRepositoryBranch, - deploymentRepositoryPath: deploymentRepositoryPath, - ocmConfig: ocmConfig, - gitCredentials: gitCredentials, - fluxcdNamespace: fluxcdNamespace, - platformKubeconfig: platformKubeconfig, - platformCluster: platformCluster, - log: log, + Config: config, + GitConfigPath: gitConfigPath, + OcmConfigPath: ocmConfigPath, + platformCluster: platformCluster, + fluxNamespace: FluxSystemNamespace, + log: log, } } -func (d *FluxDeployer) Deploy(ctx context.Context) error { +func (d *FluxDeployer) Deploy(ctx context.Context) (err error) { + d.log.Infof("Ensure namespace %s exists", d.fluxNamespace) + namespaceMutator := resources.NewNamespaceMutator(d.fluxNamespace) + if err := resources.CreateOrUpdateResource(ctx, d.platformCluster.Client(), namespaceMutator); err != nil { + return fmt.Errorf("error creating/updating namespace %s: %w", d.fluxNamespace, err) + } - // Get root component and gitops-templates component. - d.log.Info("Loading root component and gitops-templates component") - componentGetter := ocmcli.NewComponentGetter(d.componentLocation, d.deploymentTemplates, d.ocmConfig) - if err := componentGetter.InitializeComponents(ctx); err != nil { + if err := CreateGitCredentialsSecret(ctx, d.log, d.GitConfigPath, GitSecretName, d.fluxNamespace, d.platformCluster.Client()); err != nil { return err } - // Create a temporary directory to store the downloaded resource - d.log.Info("Creating download directory for gitops-templates") - downloadDir, err := os.MkdirTemp("", "flux-resource-") + // Create temporary working directory + d.log.Info("Creating working directory for gitops-templates") + d.workDir, err = util.CreateTempDir() if err != nil { - return fmt.Errorf("error creating temporary download directory for flux resource: %w", err) + return fmt.Errorf("error creating temporary working directory for flux resource: %w", err) } defer func() { - if err := os.RemoveAll(downloadDir); err != nil { - fmt.Printf("error removing temporary download directory for flux resource: %v\n", err) + err := util.DeleteTempDir(d.workDir) + if err != nil { + fmt.Printf("error removing temporary working directory for flux resource: %v\n", err) } }() - d.log.Debugf("Download directory: %s", downloadDir) + d.log.Tracef("Created working directory: %s", d.workDir) + + d.downloadDir = filepath.Join(d.workDir, "download") + d.log.Tracef("Creating download directory: %s", d.downloadDir) + err = os.MkdirAll(d.downloadDir, 0755) + if err != nil { + return fmt.Errorf("failed to create download directory: %w", err) + } + d.log.Tracef("Created download directory: %s", d.downloadDir) + + d.templatesDir = filepath.Join(d.workDir, "templates") + d.log.Tracef("Creating templates directory: %s", d.templatesDir) + err = os.MkdirAll(d.templatesDir, 0755) + if err != nil { + return fmt.Errorf("failed to create templates directory: %w", err) + } + d.log.Tracef("Created templates directory: %s", d.templatesDir) + + d.repoDir = filepath.Join(d.workDir, "repo") + d.log.Tracef("Creating repo directory: %s", d.repoDir) + err = os.MkdirAll(d.repoDir, 0755) + if err != nil { + return fmt.Errorf("failed to create repo directory: %w", err) + } + d.log.Tracef("Created repo directory: %s", d.repoDir) + + // Get components + // - root component + // - gitops-templates component + // - that contains the image resources for fluxcd source controller component + d.log.Info("Loading root component and gitops-templates component") + componentGetter := ocmcli.NewComponentGetter(d.Config.Component.OpenMCPComponentLocation, d.Config.Component.FluxcdTemplateResourcePath, d.OcmConfigPath) + if err := componentGetter.InitializeComponents(ctx); err != nil { + return err + } + + d.fluxcdCV, err = componentGetter.GetComponentVersionForResourceRecursive(ctx, componentGetter.RootComponentVersion(), FluxCDSourceControllerResourceName) + if err != nil { + return fmt.Errorf("failed to get fluxcd source controller component version: %w", err) + } // Download resource from gitops-templates component into the download directory d.log.Info("Downloading gitops-templates") - if err := componentGetter.DownloadTemplatesResource(ctx, downloadDir); err != nil { + if err := componentGetter.DownloadTemplatesResource(ctx, d.downloadDir); err != nil { return fmt.Errorf("error downloading templates: %w", err) } - if err := d.DeployFluxControllers(ctx, componentGetter.RootComponentVersion(), downloadDir); err != nil { - return fmt.Errorf("error deploying flux controllers: %w", err) + // Copy files from /download to /templates, re-arranging the directory structure as needed for kustomize + if err := d.ArrangeTemplates(); err != nil { + return fmt.Errorf("error arranging templates directory: %w", err) } - if err := d.establishFluxSync(ctx, downloadDir); err != nil { - return fmt.Errorf("error establishing flux synchronization: %w", err) + // Template all files in /templates, and write the result to /repo + if err := d.Template(); err != nil { + return fmt.Errorf("error templating files: %w", err) + } + + // Kustomize /repo/envs//fluxcd + fluxCDEnvDir := filepath.Join(d.repoDir, EnvsDirectoryName, d.Config.Environment, FluxCDDirectoryName) + manifests, err := d.Kustomize(fluxCDEnvDir) + if err != nil { + return fmt.Errorf("error kustomizing templated files: %w", err) + } + + // Apply manifests to the platform cluster + d.log.Info("Applying flux deployment objects") + if err := util.ApplyManifests(ctx, d.platformCluster, manifests); err != nil { + return err } return nil } -func (d *FluxDeployer) DeployFluxControllers(ctx context.Context, rootComponentVersion *ocmcli.ComponentVersion, downloadDir string) error { - d.log.Info("Deploying flux") +// ArrangeTemplates fills the templates directory with the files from the download directory, adjusting the directory structure as needed for the kustomization. +func (d *FluxDeployer) ArrangeTemplates() (err error) { + d.log.Info("Arranging template files") - images, err := GetFluxCDImages(rootComponentVersion) + // Create directory /envs//fluxcd + fluxCDEnvDir := filepath.Join(d.templatesDir, EnvsDirectoryName, d.Config.Environment, FluxCDDirectoryName) + err = os.MkdirAll(fluxCDEnvDir, 0755) if err != nil { - return fmt.Errorf("error getting images for flux controllers: %w", err) + return fmt.Errorf("failed to create fluxcd environment directory: %w", err) } - // Read manifest file - filepath := path.Join(downloadDir, "resources", "gotk-components.yaml") - d.log.Debugf("Reading flux deployment objects from file %s", filepath) - manifestTpl, err := d.readFileContent(filepath) + // Create directory /resources/fluxcd + fluxCDResourcesDir := filepath.Join(d.templatesDir, ResourcesDirectoryName, FluxCDDirectoryName) + err = os.MkdirAll(fluxCDResourcesDir, 0755) if err != nil { - return fmt.Errorf("error reading flux deployment objects from file %s: %w", filepath, err) + return fmt.Errorf("failed to create fluxcd resources directory: %w", err) } - // Template - values := map[string]any{ - "Values": map[string]any{ - "namespace": d.fluxcdNamespace, - "images": images, - }, + d.log.Debug("Copying template files to target directories") + + // copy all files from /templates/overlays to /envs//fluxcd + err = util.CopyDir(filepath.Join(d.downloadDir, TemplatesDirectoryName, OverlaysDirectoryName), fluxCDEnvDir) + if err != nil { + return fmt.Errorf("failed to copy fluxcd overlays: %w", err) } - d.log.Debug("Templating flux deployment objects") - manifest, err := template.NewTemplateExecution().Execute("flux-deployment", string(manifestTpl), values) + + // copy all files from /templates/resources to /resources/fluxcd + err = util.CopyDir(filepath.Join(d.downloadDir, TemplatesDirectoryName, ResourcesDirectoryName), fluxCDResourcesDir) if err != nil { - return fmt.Errorf("error templating flux deployment objects: %w", err) + return fmt.Errorf("failed to copy fluxcd resources: %w", err) } - // Apply - d.log.Debug("Applying flux deployment objects") - if err := util.ApplyManifests(ctx, d.platformCluster, manifest); err != nil { - return err + d.log.Info("Arranged template files") + return nil +} + +// Template templates the files in the templates directory, replacing placeholders with actual values. +// The resulting files are written to the repo directory. +func (d *FluxDeployer) Template() (err error) { + d.log.Infof("Applying templates from %s to deployment repository", d.Config.Component.FluxcdTemplateResourcePath) + templateInput := NewTemplateInputFromConfig(d.Config) + + if err = templateInput.AddImageResource(d.fluxcdCV, FluxCDSourceControllerResourceName, "sourceController"); err != nil { + return fmt.Errorf("failed to apply fluxcd source controller template input: %w", err) + } + if err = templateInput.AddImageResource(d.fluxcdCV, FluxCDKustomizationControllerResourceName, "kustomizeController"); err != nil { + return fmt.Errorf("failed to apply fluxcd kustomize controller template input: %w", err) + } + if err = templateInput.AddImageResource(d.fluxcdCV, FluxCDHelmControllerResourceName, "helmController"); err != nil { + return fmt.Errorf("failed to apply fluxcd helm controller template input: %w", err) + } + + if err = TemplateDirectory(d.templatesDir, templateInput, d.repoDir, d.log); err != nil { + return fmt.Errorf("failed to apply templates from directory %s: %w", d.templatesDir, err) } return nil } -func (d *FluxDeployer) establishFluxSync(ctx context.Context, downloadDir string) error { - d.log.Info("Establishing flux synchronization with deployment repository") +// Kustomize runs kustomize on the given directory and returns the resulting yaml as a byte slice. +func (d *FluxDeployer) Kustomize(dir string) ([]byte, error) { + d.log.Infof("Kustomizing files in directory: %s", dir) + fs := filesys.MakeFsOnDisk() + opts := krusty.MakeDefaultOptions() + kustomizer := krusty.MakeKustomizer(opts) + + resourceMap, err := kustomizer.Run(fs, dir) + if err != nil { + return nil, fmt.Errorf("error running kustomization: %w", err) + } - const secretName = "git" + resourcesYaml, err := resourceMap.AsYaml() + if err != nil { + return nil, fmt.Errorf("error converting resources to yaml: %w", err) + } - if err := CreateGitCredentialsSecret(ctx, d.log, d.gitCredentials, secretName, d.fluxcdNamespace, d.platformCluster.Client()); err != nil { - return err + return resourcesYaml, nil +} + +func (d *FluxDeployer) DeployFluxControllers(ctx context.Context, rootComponentVersion *ocmcli.ComponentVersion, downloadDir string) error { + d.log.Info("Deploying flux") + + images, err := GetFluxCDImages(rootComponentVersion) + if err != nil { + return fmt.Errorf("error getting images for flux controllers: %w", err) } // Read manifest file - filepath := path.Join(downloadDir, "resources", "gotk-sync.yaml") - d.log.Debugf("Reading flux synchronization objects from file %s", filepath) + filepath := path.Join(downloadDir, "resources", "gotk-components.yaml") + d.log.Debugf("Reading flux deployment objects from file %s", filepath) manifestTpl, err := d.readFileContent(filepath) if err != nil { - return fmt.Errorf("error reading manifests for flux sync: %w", err) + return fmt.Errorf("error reading flux deployment objects from file %s: %w", filepath, err) } // Template - d.log.Debug("Templating flux synchronization objects") values := map[string]any{ "Values": map[string]any{ - "namespace": d.fluxcdNamespace, - "git": map[string]any{ - "repoUrl": d.deploymentRepository, - "mainBranch": d.deploymentRepositoryBranch, - "path": d.deploymentRepositoryPath, - "secretName": secretName, - }, + "namespace": d.fluxNamespace, + "images": images, }, } - manifest, err := template.NewTemplateExecution().Execute("flux-sync", string(manifestTpl), values) + d.log.Debug("Templating flux deployment objects") + manifest, err := template.NewTemplateExecution().Execute("flux-deployment", string(manifestTpl), values) if err != nil { - return fmt.Errorf("error templating flux synchronization objects: %w", err) + return fmt.Errorf("error templating flux deployment objects: %w", err) } // Apply - d.log.Debug("Applying flux synchronization objects") + d.log.Debug("Applying flux deployment objects") if err := util.ApplyManifests(ctx, d.platformCluster, manifest); err != nil { return err } diff --git a/internal/flux_deployer/deployer_test.go b/internal/flux_deployer/deployer_test.go index 969fa15..695ad63 100644 --- a/internal/flux_deployer/deployer_test.go +++ b/internal/flux_deployer/deployer_test.go @@ -9,6 +9,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + cfg "github.com/openmcp-project/bootstrapper/internal/config" "github.com/openmcp-project/bootstrapper/internal/flux_deployer" logging "github.com/openmcp-project/bootstrapper/internal/log" ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" @@ -21,10 +22,20 @@ func TestDeployFluxController(t *testing.T) { platformClient := fake.NewClientBuilder().Build() platformCluster := clusters.NewTestClusterFromClient("platform", platformClient) - namespace := "flux-system-test" - - d := flux_deployer.NewFluxDeployer("", "", "", - "", "", ocmcli.NoOcmConfig, "", namespace, "", platformCluster, logging.GetLogger()) + namespace := flux_deployer.FluxSystemNamespace + + config := &cfg.BootstrapperConfig{ + Component: cfg.Component{ + OpenMCPComponentLocation: "./testdata/01/root-component-version-1.yaml", + }, + DeploymentRepository: cfg.DeploymentRepository{}, + Providers: cfg.Providers{}, + ImagePullSecrets: nil, + OpenMCPOperator: cfg.OpenMCPOperator{}, + Environment: "", + } + + d := flux_deployer.NewFluxDeployer(config, "", ocmcli.NoOcmConfig, platformCluster, logging.GetLogger()) // Create a deployment err := d.DeployFluxControllers(t.Context(), rootComponentVersion1, downloadDir) diff --git a/internal/flux_deployer/git_credentials.go b/internal/flux_deployer/git_credentials.go index d843f58..fa4c19b 100644 --- a/internal/flux_deployer/git_credentials.go +++ b/internal/flux_deployer/git_credentials.go @@ -26,7 +26,7 @@ const ( // The file should contain a YAML of a map[string]string, whose keys are described // in https://fluxcd.io/flux/components/source/gitrepositories/#secret-reference, e.g. username and password. func CreateGitCredentialsSecret(ctx context.Context, log *logrus.Logger, gitCredentialsPath string, secretName, secretNamespace string, platformClient client.Client) error { - log.Debug("Creating or updating git credentials secret") + log.Infof("Creating/updating git credentials secret %s/%s", secretNamespace, secretName) gitCredentialsData := map[string][]byte{} @@ -80,5 +80,6 @@ func CreateGitCredentialsSecret(ctx context.Context, log *logrus.Logger, gitCred return fmt.Errorf("error creating or updating git credentials secret: %w", err) } + log.Infof("Created/updated git credentials secret %s/%s", secretNamespace, secretName) return nil } diff --git a/internal/flux_deployer/images.go b/internal/flux_deployer/images.go index af851e1..9b0d05b 100644 --- a/internal/flux_deployer/images.go +++ b/internal/flux_deployer/images.go @@ -8,7 +8,7 @@ import ( // GetFluxCDImages retrieves the images of the FluxCD controllers from the root component version. func GetFluxCDImages(rootCV *ocmcli.ComponentVersion) (map[string]any, error) { - return GetImages(rootCV, FluxcdHelmController, FluxcdKustomizeController, FluxcdSourceController) + return GetImages(rootCV, FluxCDHelmControllerResourceName, FluxCDKustomizationControllerResourceName, FluxCDSourceControllerResourceName) } // GetImages retrieves the images references for a list of resources of a component version. diff --git a/internal/flux_deployer/images_test.go b/internal/flux_deployer/images_test.go index 38e91b9..d527304 100644 --- a/internal/flux_deployer/images_test.go +++ b/internal/flux_deployer/images_test.go @@ -13,13 +13,13 @@ func TestGetFluxCDImages(t *testing.T) { imageMap, err := flux_deployer.GetFluxCDImages(cv) assert.NoError(t, err, "error getting images") - img, found := imageMap[flux_deployer.FluxcdSourceController] + img, found := imageMap[flux_deployer.FluxCDSourceControllerResourceName] assert.True(t, found, "fluxcd source controller image should be found") assert.Equal(t, "test-source-controller-image:v0.0.1", img, "fluxcd source controller image should match") - img, found = imageMap[flux_deployer.FluxcdHelmController] + img, found = imageMap[flux_deployer.FluxCDHelmControllerResourceName] assert.True(t, found, "fluxcd helm controller image should be found") assert.Equal(t, "test-helm-controller-image:v0.0.1", img, "fluxcd helm controller image should match") - img, found = imageMap[flux_deployer.FluxcdKustomizeController] + img, found = imageMap[flux_deployer.FluxCDKustomizationControllerResourceName] assert.True(t, found, "fluxcd kustomize controller image should be found") assert.Equal(t, "test-kustomize-controller-image:v0.0.1", img, "fluxcd kustomize controller image should match") } diff --git a/internal/flux_deployer/template.go b/internal/flux_deployer/template.go new file mode 100644 index 0000000..50c8a95 --- /dev/null +++ b/internal/flux_deployer/template.go @@ -0,0 +1,95 @@ +package flux_deployer + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" + + "github.com/openmcp-project/bootstrapper/internal/template" +) + +// TemplateDirectory processes the template files in the specified directory and writes +// the rendered content to the corresponding files in the Git repository's worktree. +// It uses the provided template directory and Git repository to perform the operations. +func TemplateDirectory(templateDirectory string, templateInput TemplateInput, repo string, logger *logrus.Logger) error { + + templateDir, err := os.Open(templateDirectory) + if err != nil { + return fmt.Errorf("failed to open template directory: %w", err) + } + defer func() { + if err = templateDir.Close(); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to close template directory: %v\n", err) + } + }() + + te := template.NewTemplateExecution().WithMissingKeyOption("zero") + + // Recursively walk through all files in the template directory + err = filepath.WalkDir(templateDirectory, func(path string, d os.DirEntry, walkError error) error { + var ( + errInWalk error + + templateFromFile []byte + templateResult []byte + + relativePath string + fileInWorkTree *os.File + ) + + if walkError != nil { + return walkError + } + + relativePath, errInWalk = filepath.Rel(templateDirectory, path) + if errInWalk != nil { + return fmt.Errorf("failed to get relative path for %s: %w", path, errInWalk) + } + pathInWorkTree := filepath.Join(repo, relativePath) + + if d.IsDir() { + err = os.MkdirAll(pathInWorkTree, 0755) + if err != nil { + return fmt.Errorf("failed to create directory %s: %w", path, err) + } + } else { + logger.Debugf("Found template file: %s", relativePath) + + templateFromFile, errInWalk = os.ReadFile(path) + if errInWalk != nil { + return fmt.Errorf("failed to read template file %s: %w", relativePath, err) + } + + templateResult, errInWalk = te.Execute(path, string(templateFromFile), templateInput.ValuesWrapper()) + if errInWalk != nil { + return fmt.Errorf("failed to execute template %s: %w", relativePath, errInWalk) + } + + fileInWorkTree, errInWalk = os.Create(pathInWorkTree) + if errInWalk != nil { + return fmt.Errorf("failed to open file in worktree %s: %w", relativePath, errInWalk) + } + defer func(pathInRepo *os.File) { + err := pathInRepo.Close() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "failed to close file in worktree %s: %v\n", relativePath, err) + } + }(fileInWorkTree) + + _, errInWalk = fileInWorkTree.Write(templateResult) + if errInWalk != nil { + return fmt.Errorf("failed to write to file in worktree %s: %w", relativePath, errInWalk) + } + } + + return nil + }) + + if err != nil { + return fmt.Errorf("failed to walk template directory: %w", err) + } + + return nil +} diff --git a/internal/flux_deployer/template_input.go b/internal/flux_deployer/template_input.go new file mode 100644 index 0000000..f1b6517 --- /dev/null +++ b/internal/flux_deployer/template_input.go @@ -0,0 +1,71 @@ +package flux_deployer + +import ( + "fmt" + + "github.com/openmcp-project/bootstrapper/internal/config" + ocmcli "github.com/openmcp-project/bootstrapper/internal/ocm-cli" + "github.com/openmcp-project/bootstrapper/internal/util" +) + +type TemplateInput map[string]any + +func NewTemplateInput() TemplateInput { + return make(TemplateInput) +} + +func NewTemplateInputFromConfig(c *config.BootstrapperConfig) TemplateInput { + t := TemplateInput{ + "fluxCDEnvPath": "./" + EnvsDirectoryName + "/" + c.Environment + "/" + FluxCDDirectoryName, + "fluxCDResourcesPath": "../../../" + ResourcesDirectoryName + "/" + FluxCDDirectoryName, + "openMCPResourcesPath": "../../../" + ResourcesDirectoryName + "/" + OpenMCPDirectoryName, + + "git": map[string]interface{}{ + "repoUrl": c.DeploymentRepository.RepoURL, + "mainBranch": c.DeploymentRepository.RepoBranch, + }, + "gitRepoEnvBranch": c.DeploymentRepository.RepoBranch, + + "imagePullSecrets": wrapImagePullSecrets(c.ImagePullSecrets), + } + + return t +} + +func (t TemplateInput) AddImageResource(cv *ocmcli.ComponentVersion, resourceName, key string) error { + resource, err := cv.GetResource(resourceName) + if err != nil { + return fmt.Errorf("failed to get resource %s: %w", resourceName, err) + } + imageName, imageTag, imageDigest, err := util.ParseImageVersionAndTag(*resource.Access.ImageReference) + if err != nil { + return fmt.Errorf("failed to parse image reference %s: %w", *resource.Access.ImageReference, err) + } + + if _, found := t["images"]; !found { + t["images"] = make(map[string]any) + } + t["images"].(map[string]any)[key] = map[string]any{ + "version": imageTag, + "image": imageName, + "tag": imageTag, + "digest": imageDigest, + } + return nil +} + +func (t TemplateInput) ValuesWrapper() map[string]any { + return map[string]any{ + "Values": t, + } +} + +func wrapImagePullSecrets(secrets []string) []map[string]string { + wrappedSecrets := make([]map[string]string, len(secrets)) + for i, secret := range secrets { + wrappedSecrets[i] = map[string]string{ + "name": secret, + } + } + return wrappedSecrets +}