├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── cmd ├── helmrelease.go ├── helmrelease_test.go ├── kustomization.go ├── kustomization_test.go ├── root.go ├── scan.go └── version.go ├── examples ├── helm │ ├── README.md │ ├── manifest-1.yaml │ └── manifest-2.yaml └── kustomization │ ├── README.md │ ├── manifest-1.yaml │ └── manifest-2.yaml ├── go.mod ├── go.sum ├── main.go └── pkg ├── argo └── argo.go └── utils └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | dist/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_DIR := $(shell pwd) 2 | DIST_DIR := $(CURRENT_DIR)/dist 3 | CLI_NAME := mta 4 | BIN_NAME := mta 5 | CGO_FLAG := 0 6 | 7 | HOST_OS := $(shell go env GOOS) 8 | HOST_ARCH := $(shell go env GOARCH) 9 | 10 | TARGET_ARCH ?= linux/amd64 11 | TARGET_OS := $(shell echo $(TARGET_ARCH) | cut -d'/' -f1) 12 | TARGET_ARCH_SHORT := $(shell echo $(TARGET_ARCH) | cut -d'/' -f2) 13 | 14 | VERSION := $(shell cat ${CURRENT_DIR}/VERSION) 15 | 16 | GOPATH ?= $(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi) 17 | GOCACHE ?= $(HOME)/.cache/go-build 18 | ARGOCD_LINT_GOGC ?= 20 19 | 20 | default: build 21 | 22 | $(DIST_DIR): 23 | mkdir -p $(DIST_DIR) 24 | 25 | .PHONY: cli 26 | cli-local: 27 | GOOS=$(HOST_OS) GOARCH=$(HOST_ARCH) CGO_ENABLED=$(CGO_FLAG) go build -o $(DIST_DIR)/$(CLI_NAME) . 28 | 29 | .PHONY: build 30 | build: $(DIST_DIR) 31 | GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH_SHORT) CGO_ENABLED=$(CGO_FLAG) go build -o $(DIST_DIR)/$(CLI_NAME) . 32 | 33 | .PHONY: run 34 | run: 35 | go run main.go 36 | 37 | .PHONY: lint 38 | lint: 39 | golangci-lint --version 40 | # NOTE: If you get an "Out of Memory" error, try reducing GOGC value 41 | GOGC=$(ARGOCD_LINT_GOGC) GOMAXPROCS=2 golangci-lint run --fix --verbose 42 | 43 | .PHONY: test 44 | test: 45 | go test -v ./... 46 | 47 | .PHONY: fmt 48 | fmt: 49 | go fmt ./... 50 | 51 | .PHONY: tidy 52 | tidy: 53 | go mod tidy 54 | 55 | .PHONY: deps 56 | deps: 57 | go list -m -u all 58 | 59 | .PHONY: clean 60 | clean: 61 | rm -rf $(DIST_DIR) 62 | 63 | .PHONY: release 64 | release: 65 | @echo "Building release version $(VERSION)" 66 | GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH_SHORT) CGO_ENABLED=$(CGO_FLAG) go build -o $(DIST_DIR)/$(CLI_NAME)-$(VERSION) . 67 | 68 | .PHONY: help 69 | help: 70 | @echo "Usage: make [target]" 71 | @echo "" 72 | @echo "Targets:" 73 | @echo " build Compile the project" 74 | @echo " cli-local Build for local system" 75 | @echo " run Run the Go application" 76 | @echo " lint Run golangci-lint" 77 | @echo " test Run unit tests" 78 | @echo " fmt Format the code" 79 | @echo " tidy Clean up go.mod dependencies" 80 | @echo " deps Check for outdated dependencies" 81 | @echo " clean Remove build files" 82 | @echo " release Build a release version" 83 | @echo " help Show this help message" 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mta (Migrate to Argo CD) 2 | 3 | The `mta` cli will export Flux components to Argo CD consumable 4 | CRs. This can be used in order to help migrating from Flux to Argo 5 | CD. This is in the "proof of concept" phase and we make no guarantees. 6 | 7 | We love feedback! Come join us at our [Akuity Discord Community](https://akuity.community)! 8 | 9 | # Installation 10 | 11 | Install the `mta` binary from the [releases page](https://github.com/akuity/mta/releases) in your `$PATH`. 12 | 13 | There is shell completion for convenience. 14 | 15 | > *NOTE* it's probably `zsh` on a Mac 16 | 17 | ```shell 18 | mta completion bash 19 | ``` 20 | 21 | # Quickstart 22 | 23 | Here's a video which explains how to use 'mta' to export Flux components to Argo CD consumable CRs. 24 | 25 | [![Introduction to 'mta' - Migrate from Flux to Argo CD](https://img.youtube.com/vi/c8sP0QSOfsg/0.jpg)](https://www.youtube.com/watch?v=c8sP0QSOfsg) 26 | 27 | Below are some examples 28 | 29 | ## Manual Migration 30 | 31 | > *NOTE*: See the [Flux Documentation](https://fluxcd.io/flux/get-started/) for more information about Flux. 32 | 33 | After downloading the binary, you can scan your system for `HelmReleases` and `Kustomizations`. Example: 34 | 35 | ```shell 36 | $ mta scan 37 | ┌───────────────┬─────────────┬─────────────┬─────────────────────────────────────────────────────────────────┐ 38 | │ KIND │ NAME │ NAMESPACE │ STATUS │ 39 | ├───────────────┼─────────────┼─────────────┼─────────────────────────────────────────────────────────────────┤ 40 | │ HelmRelease │ podinfo │ flux-system │ Release reconciliation succeeded │ 41 | │ HelmRelease │ sample │ flux-system │ Release reconciliation succeeded │ 42 | ├───────────────┼─────────────┼─────────────┼─────────────────────────────────────────────────────────────────┤ 43 | │ Kustomization │ flux-system │ flux-system │ Applied revision: main/f35c47113103d67b20859a2301fa5c88a8f7c6c9 │ 44 | └───────────────┴─────────────┴─────────────┴─────────────────────────────────────────────────────────────────┘ 45 | ``` 46 | 47 | You can then, migrate them over; for example to migrate the `HelmRelease` called `sample` (in my above example), you can do: 48 | 49 | ```shell 50 | $ mta helmrelease --name sample 51 | ``` 52 | 53 | You'll see the Argo CD Application that will be created: 54 | 55 | ```yaml 56 | apiVersion: argoproj.io/v1alpha1 57 | kind: Application 58 | metadata: 59 | name: sample 60 | namespace: argocd 61 | spec: 62 | destination: 63 | namespace: quarkus 64 | server: https://kubernetes.default.svc 65 | project: default 66 | source: 67 | chart: quarkus 68 | helm: 69 | values: | 70 | build: 71 | enabled: false 72 | deploy: 73 | route: 74 | enabled: false 75 | image: 76 | name: quay.io/ablock/gitops-helm-quarkus 77 | repoURL: https://redhat-developer.github.io/redhat-helm-charts 78 | targetRevision: 0.0.3 79 | syncPolicy: 80 | automated: 81 | prune: true 82 | selfHeal: true 83 | syncOptions: 84 | - CreateNamespace=true 85 | - Validate=false 86 | ``` 87 | 88 | You can pipe this into `kubectl apply` or you can have `mta` do it for you 89 | 90 | > *NOTE* You'll have to install Argo CD before running this command 91 | 92 | ```shell 93 | $ mta helmrelease --name sample --confirm-migrate 94 | ``` 95 | 96 | The same can be done for `Kustomizations`, example: 97 | 98 | > *NOTE* `Kustomizations`, because of the nature of how they are setup, are migrated via an ApplicationSet 99 | 100 | ```shell 101 | $ mta kustomization --name flux-system --confirm-migrate 102 | ``` 103 | 104 | By default, the ApplicationSet created from the `Kustomiation` will exclude the `flux-system` directory. You can exclude other directories that have Flux specific Kubernetes objects by passing the `--exclude-dirs` option. 105 | 106 | ```shell 107 | $ mta kustomization --name flux-system --exclude-dirs flux-system-extras --confirm-migrate 108 | ``` 109 | 110 | > *NOTE* To exclude more directories, you an pass a comma separated list to `--exclude-dirs`. Example: `--exclude-dirs foo,bar,bazz`. You can also pass `--exclude-dirs` to the `scan` command as well. 111 | 112 | For a detailed list of examples, read the [examples](./examples) docs. 113 | 114 | ## Auto Migration 115 | 116 | You can have the `scan` subcommand automatically migrate everything for you 117 | 118 | > :bangbang: *NOTE* This option also _deletes_ Flux from the system. Use with caution 119 | 120 | ```shell 121 | $ mta scan --auto-migrate 122 | ``` 123 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.9 -------------------------------------------------------------------------------- /cmd/helmrelease.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Christian Hernandez christian@chernand.io 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | "strconv" 23 | 24 | argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 25 | yaml "sigs.k8s.io/yaml" 26 | 27 | "github.com/akuity/mta/pkg/argo" 28 | "github.com/akuity/mta/pkg/utils" 29 | helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" 30 | sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" 31 | log "github.com/sirupsen/logrus" 32 | "github.com/spf13/cobra" 33 | "k8s.io/apimachinery/pkg/runtime" 34 | "k8s.io/apimachinery/pkg/types" 35 | "k8s.io/cli-runtime/pkg/printers" 36 | client "sigs.k8s.io/controller-runtime/pkg/client" 37 | ) 38 | 39 | // helmreleaseCmd represents the helmrelease command 40 | var helmreleaseCmd = &cobra.Command{ 41 | Use: "helmrelease", 42 | Aliases: []string{"HelmRelease", "hr", "helmreleases"}, 43 | Short: "Exports a HelmRelease into an Application", 44 | Long: `This migration tool helps you move your Flux HelmReleases into Argo CD 45 | Applications. Example: 46 | 47 | mta helmrelease --name=myhelmrelease --namespace=flux-system | kubectl apply -n argocd -f - 48 | 49 | This utilty exports the named HelmRelease and the source Helm repo and 50 | creates a manifests to stdout, which you can pipe into an apply command 51 | with kubectl.`, 52 | Run: func(cmd *cobra.Command, args []string) { 53 | // Get the Argo CD namespace 54 | argoCDNamespace, err := cmd.Flags().GetString("argocd-namespace") 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | // Get the options from the CLI 60 | kubeConfig, err := cmd.Flags().GetString("kubeconfig") 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | helmReleaseName, _ := cmd.Flags().GetString("name") 65 | helmReleaseNamespace, _ := cmd.Flags().GetString("namespace") 66 | confirmMigrate, _ := cmd.Flags().GetBool("confirm-migrate") 67 | 68 | // Set up the default context 69 | ctx := context.TODO() 70 | 71 | // Set up the schema because HelmRelease and Repo is a CRD 72 | scheme := runtime.NewScheme() 73 | helmv2.AddToScheme(scheme) 74 | sourcev1.AddToScheme(scheme) 75 | argov1alpha1.AddToScheme(scheme) 76 | 77 | // create rest config using the kubeconfig file. 78 | restConfig, err := utils.NewRestConfig(kubeConfig) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | // Create a new client based on the restconfig and scheme 84 | k, err := client.New(restConfig, client.Options{ 85 | Scheme: scheme, 86 | }) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | //Get the helmrelease based on type, report if there's an error 92 | helmRelease := &helmv2.HelmRelease{} 93 | err = k.Get(ctx, types.NamespacedName{Namespace: helmReleaseNamespace, Name: helmReleaseName}, helmRelease) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | helmRepoNamespace := GetHelmRepoNamespace(helmRelease) 98 | 99 | // Get the helmrepo based on type, report if error 100 | helmRepo := &sourcev1.HelmRepository{} 101 | err = k.Get(ctx, types.NamespacedName{Namespace: helmRepoNamespace, Name: helmRelease.Spec.Chart.Spec.SourceRef.Name}, helmRepo) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | // Get the helmchart based on type, report if error 107 | helmChartName := fmt.Sprintf("%s-%s", helmReleaseNamespace, helmReleaseName) 108 | 109 | helmChart := &sourcev1.HelmChart{} 110 | err = k.Get(ctx, types.NamespacedName{Namespace: helmRepoNamespace, Name: helmChartName}, helmChart) 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | 115 | // Get the Values from the HelmRelease 116 | yaml, err := yaml.Marshal(helmRelease.Spec.Values) 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | 121 | helmAppNamePrefix := helmRelease.Spec.TargetNamespace 122 | if helmAppNamePrefix == "" { 123 | helmAppNamePrefix = helmReleaseNamespace 124 | } 125 | // Generate the Argo CD Helm Application 126 | helmApp := argo.ArgoCdHelmApplication{ 127 | //Name: helmRelease.Name, 128 | Name: helmAppNamePrefix + "-" + helmRelease.Name, 129 | Namespace: argoCDNamespace, 130 | DestinationNamespace: helmRelease.Spec.TargetNamespace, 131 | DestinationServer: "https://kubernetes.default.svc", 132 | Project: "default", 133 | HelmChart: helmRelease.Spec.Chart.Spec.Chart, 134 | HelmRepo: helmRepo.Spec.URL, 135 | HelmTargetRevision: helmRelease.Spec.Chart.Spec.Version, 136 | HelmValues: string(yaml), 137 | HelmCreateNamespace: strconv.FormatBool(helmRelease.Spec.Install.CreateNamespace), 138 | } 139 | 140 | helmArgoCdApp, err := argo.GenArgoCdHelmApplication(helmApp) 141 | if err != nil { 142 | log.Fatal(err) 143 | } 144 | 145 | // Do the migration automatically if that is set, if not print to stdout 146 | if confirmMigrate { 147 | log.Info("Migrating HelmRelease \"" + helmRelease.Name + "\" to Argo CD via an Application") 148 | // Suspend helm reconciliation 149 | if err := utils.SuspendFluxObject(k, ctx, helmRelease); err != nil { 150 | log.Fatal(err) 151 | } 152 | 153 | // suspend helm repo reconciliation 154 | if err := utils.SuspendFluxObject(k, ctx, helmRepo); err != nil { 155 | log.Fatal(err) 156 | } 157 | 158 | // suspend helm chart 159 | if err := utils.SuspendFluxObject(k, ctx, helmChart); err != nil { 160 | log.Fatal(err) 161 | } 162 | 163 | // Finally, create the Argo CD Application 164 | if err := utils.CreateK8SObjects(k, ctx, helmArgoCdApp); err != nil { 165 | log.Fatal(err) 166 | } 167 | 168 | // Delete the HelmRelease 169 | if err := utils.DeleteK8SObjects(k, ctx, helmRelease); err != nil { 170 | log.Fatal(err) 171 | } 172 | 173 | // Delete the HelmRepo 174 | if err := utils.DeleteK8SObjects(k, ctx, helmRepo); err != nil { 175 | log.Fatal(err) 176 | } 177 | 178 | // Delete the chart 179 | if err := utils.DeleteK8SObjects(k, ctx, helmChart); err != nil { 180 | log.Fatal(err) 181 | } 182 | 183 | } else { 184 | // Set the printer type to YAML 185 | printr := printers.NewTypeSetter(k.Scheme()).ToPrinter(&printers.YAMLPrinter{}) 186 | 187 | // print the AppSet YAML to Strdout 188 | if err := printr.PrintObj(helmArgoCdApp, os.Stdout); err != nil { 189 | log.Fatal(err) 190 | } 191 | } 192 | 193 | }, 194 | } 195 | 196 | func GetHelmRepoNamespace(helmRelease *helmv2.HelmRelease) string { 197 | helmRepoNamespace := helmRelease.Spec.Chart.Spec.SourceRef.Namespace 198 | if helmRepoNamespace == "" { 199 | helmRepoNamespace = helmRelease.Namespace 200 | } 201 | 202 | return helmRepoNamespace 203 | } 204 | 205 | func init() { 206 | rootCmd.AddCommand(helmreleaseCmd) 207 | rootCmd.MarkPersistentFlagRequired("name") 208 | 209 | helmreleaseCmd.Flags().Bool("confirm-migrate", false, "Automatically Migrate the HelmRelease to an ApplicationSet") 210 | } 211 | -------------------------------------------------------------------------------- /cmd/helmrelease_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" 5 | "github.com/magiconair/properties/assert" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "testing" 8 | ) 9 | 10 | func TestHelmGetRepoNamespace(t *testing.T) { 11 | scheme := runtime.NewScheme() 12 | helmv2.AddToScheme(scheme) 13 | 14 | helmReleaseWithNamespace := &helmv2.HelmRelease{ 15 | Spec: helmv2.HelmReleaseSpec{ 16 | Chart: helmv2.HelmChartTemplate{ 17 | Spec: helmv2.HelmChartTemplateSpec{ 18 | SourceRef: helmv2.CrossNamespaceObjectReference{ 19 | Namespace: "custom-namespace", 20 | }, 21 | }, 22 | }, 23 | }, 24 | } 25 | 26 | namespace := GetHelmRepoNamespace(helmReleaseWithNamespace) 27 | assert.Equal(t, "custom-namespace", namespace, "Expected the namespace to be 'custom-namespace'") 28 | 29 | helmReleaseWithoutNamespace := &helmv2.HelmRelease{ 30 | Spec: helmv2.HelmReleaseSpec{ 31 | Chart: helmv2.HelmChartTemplate{ 32 | Spec: helmv2.HelmChartTemplateSpec{ 33 | SourceRef: helmv2.CrossNamespaceObjectReference{}, 34 | }, 35 | }, 36 | }, 37 | } 38 | 39 | namespace = GetHelmRepoNamespace(helmReleaseWithoutNamespace) 40 | assert.Equal(t, helmReleaseWithoutNamespace.Namespace, namespace, "Expected the namespace to be 'default'") 41 | } 42 | -------------------------------------------------------------------------------- /cmd/kustomization.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Christian Hernandez christian@chernand.io 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "context" 20 | "os" 21 | "strings" 22 | 23 | "github.com/akuity/mta/pkg/argo" 24 | "github.com/akuity/mta/pkg/utils" 25 | argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 26 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 27 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 28 | log "github.com/sirupsen/logrus" 29 | "github.com/spf13/cobra" 30 | corev1 "k8s.io/api/core/v1" 31 | "k8s.io/apimachinery/pkg/runtime" 32 | "k8s.io/apimachinery/pkg/types" 33 | "k8s.io/cli-runtime/pkg/printers" 34 | client "sigs.k8s.io/controller-runtime/pkg/client" 35 | ) 36 | 37 | // kustomizationCmd represents the kustomization command 38 | var kustomizationCmd = &cobra.Command{ 39 | Use: "kustomization", 40 | Aliases: []string{"k", "kustomizations"}, 41 | Short: "Exports a Kustomization into an ApplicationSet", 42 | Long: `This is a migration tool that helps you move your Flux Kustomizations 43 | into an Argo CD ApplicationSet. Example: 44 | 45 | mta kustomization --name=mykustomization --namespace=flux-system | kubectl apply -n argocd -f - 46 | 47 | This utilty exports the named Kustomization and the source Git repo and 48 | creates a manifests to stdout, which you can pipe into an apply command 49 | with kubectl.`, 50 | Run: func(cmd *cobra.Command, args []string) { 51 | // Get excluded-dirs from the cli 52 | exd, err := cmd.Flags().GetStringSlice("exclude-dirs") 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | 57 | // Get the Argo CD namespace 58 | argoCDNamespace, err := cmd.Flags().GetString("argocd-namespace") 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | // Get the options from the CLI 64 | kubeConfig, err := cmd.Flags().GetString("kubeconfig") 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | kustomizationName, _ := cmd.Flags().GetString("name") 69 | kustomizationNamespace, _ := cmd.Flags().GetString("namespace") 70 | confirmMigrate, _ := cmd.Flags().GetBool("confirm-migrate") 71 | 72 | // Set up the default context 73 | ctx := context.TODO() 74 | 75 | // Set up the scheme of components we need 76 | scheme := runtime.NewScheme() 77 | kustomizev1.AddToScheme(scheme) 78 | sourcev1.AddToScheme(scheme) 79 | corev1.AddToScheme(scheme) 80 | argov1alpha1.AddToScheme(scheme) 81 | 82 | // create rest config using the kubeconfig file. 83 | restConfig, err := utils.NewRestConfig(kubeConfig) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | // Create a new client based on the restconfig and scheme 89 | k, err := client.New(restConfig, client.Options{ 90 | Scheme: scheme, 91 | }) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | // get the kustomization based on the type, report if there's an error 97 | kustomization := &kustomizev1.Kustomization{} 98 | err = k.Get(ctx, types.NamespacedName{Namespace: kustomizationNamespace, Name: kustomizationName}, kustomization) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | 103 | gitRepoNamespace := getGitRepoNamespace(kustomization) 104 | gitRepoName := kustomization.Spec.SourceRef.Name 105 | 106 | // get the gitsource 107 | gitSource := &sourcev1.GitRepository{} 108 | err = k.Get(ctx, types.NamespacedName{Namespace: gitRepoNamespace, Name: gitRepoName}, gitSource) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | 113 | //Get the secret holding the info we need 114 | var sshPrivateKey string 115 | if gitSource.Spec.SecretRef != nil && gitSource.Spec.SecretRef.Name != "" { 116 | secret := &corev1.Secret{} 117 | err = k.Get(ctx, types.NamespacedName{Namespace: kustomizationNamespace, Name: gitSource.Spec.SecretRef.Name}, secret) 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | 122 | sshPrivateKey = string(secret.Data["identity"]) 123 | } else { 124 | log.Info("Warning: SecretRef is not defined in the GitRepository spec. Proceeding without the SSH private key.") 125 | sshPrivateKey = "" // Leave the SSHPrivateKey empty if the secret is not available 126 | } 127 | 128 | // Argo CD ApplicationSet is sensitive about how you give it paths in the Git Dir generator. We need to figure some things out 129 | var sourcePath string 130 | var sourcePathExclude string 131 | 132 | spl := strings.SplitAfter(kustomization.Spec.Path, "./") 133 | 134 | if len(spl[1]) == 0 { 135 | sourcePath = `*` 136 | sourcePathExclude = "flux-system" 137 | } else { 138 | sourcePath = spl[1] + "/*" 139 | sourcePathExclude = spl[1] + "/flux-system" 140 | } 141 | 142 | // Add sourcePathExclude to the excludedDirs 143 | exd = append(exd, sourcePathExclude) 144 | 145 | // Generate the ApplicationSet manifest based on the struct 146 | applicationSet := argo.GitDirApplicationSet{ 147 | Namespace: argoCDNamespace, 148 | GitRepoURL: gitSource.Spec.URL, 149 | GitRepoRevision: gitSource.Spec.Reference.Branch, 150 | GitIncludeDir: sourcePath, 151 | GitExcludeDir: exd, 152 | AppName: "{{path.basename}}", 153 | AppProject: "default", 154 | AppRepoURL: gitSource.Spec.URL, 155 | AppTargetRevision: gitSource.Spec.Reference.Branch, 156 | AppPath: "{{path}}", 157 | AppDestinationServer: "https://kubernetes.default.svc", 158 | AppDestinationNamespace: kustomization.Spec.TargetNamespace, 159 | SSHPrivateKey: sshPrivateKey, 160 | GitOpsRepo: gitSource.Spec.URL, 161 | } 162 | 163 | appset, err := argo.GenGitDirAppSet(applicationSet) 164 | if err != nil { 165 | log.Fatal(err) 166 | } 167 | 168 | // Generate the ApplicationSet Secret and set the GVK 169 | appsetSecret := utils.GenK8SSecret(applicationSet) 170 | 171 | // Do the migration automatically if that is set, if not print to stdout 172 | if confirmMigrate { 173 | // Suspend kustomization reconcilation 174 | if err := utils.SuspendFluxObject(k, ctx, kustomization); err != nil { 175 | log.Fatal(err) 176 | } 177 | 178 | // Suspend the GitRepo reconcilation 179 | if err := utils.SuspendFluxObject(k, ctx, gitSource); err != nil { 180 | log.Fatal(err) 181 | } 182 | 183 | // Finally, create the ApplicationSet with the ApplicationSet Secret 184 | log.Info("Migrating Kustomization \"" + kustomization.Name + "\" to ArgoCD via an ApplicationSet") 185 | if err := utils.CreateK8SObjects(k, ctx, appsetSecret, appset); err != nil { 186 | log.Fatal(err) 187 | } 188 | 189 | // If the migration is successful, delete the Kustomization and GitRepo 190 | if err := utils.DeleteK8SObjects(k, ctx, kustomization); err != nil { 191 | log.Fatal(err) 192 | } 193 | 194 | if err := utils.DeleteK8SObjects(k, ctx, gitSource); err != nil { 195 | log.Fatal(err) 196 | } 197 | 198 | } else { 199 | // Print the ApplicationSet and Secret to stdout 200 | // Set the printer type to YAML 201 | printr := printers.NewTypeSetter(k.Scheme()).ToPrinter(&printers.YAMLPrinter{}) 202 | 203 | // Print the AppSet secret to Stdout 204 | if err := printr.PrintObj(appsetSecret, os.Stdout); err != nil { 205 | log.Fatal(err) 206 | } 207 | 208 | // print the AppSet YAML to Strdout 209 | if err := printr.PrintObj(appset, os.Stdout); err != nil { 210 | log.Fatal(err) 211 | } 212 | 213 | } 214 | 215 | }, 216 | } 217 | 218 | func getGitRepoNamespace(kustomization *kustomizev1.Kustomization) string { 219 | gitRepoNamespace := kustomization.Spec.SourceRef.Namespace 220 | if gitRepoNamespace == "" { 221 | gitRepoNamespace = kustomization.Namespace 222 | } 223 | 224 | return gitRepoNamespace 225 | } 226 | 227 | func init() { 228 | rootCmd.AddCommand(kustomizationCmd) 229 | rootCmd.MarkPersistentFlagRequired("name") 230 | 231 | kustomizationCmd.Flags().Bool("confirm-migrate", false, "Automatically Migrate the Kustomization to an ApplicationSet") 232 | kustomizationCmd.Flags().StringSlice("exclude-dirs", []string{}, "Additional Directories (besides flux-system) to exclude from the GitDir generator. Can be single or comma separated") 233 | } 234 | -------------------------------------------------------------------------------- /cmd/kustomization_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 5 | "github.com/magiconair/properties/assert" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | "testing" 8 | ) 9 | 10 | func TestKustomizeGitRepoNamespace(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | kustomization *kustomizev1.Kustomization 14 | expectedGitRepoNamespace string 15 | }{ 16 | { 17 | name: "when git repository namespace is defined", 18 | kustomization: &kustomizev1.Kustomization{ 19 | ObjectMeta: metav1.ObjectMeta{ 20 | Namespace: "kustomization-namespace", 21 | }, 22 | Spec: kustomizev1.KustomizationSpec{ 23 | SourceRef: kustomizev1.CrossNamespaceSourceReference{ 24 | Namespace: "gitrepo-namespace", 25 | }, 26 | }, 27 | }, 28 | expectedGitRepoNamespace: "gitrepo-namespace", 29 | }, 30 | { 31 | name: "when git repository namespace is not defined", 32 | kustomization: &kustomizev1.Kustomization{ 33 | ObjectMeta: metav1.ObjectMeta{ 34 | Namespace: "kustomization-namespace", 35 | }, 36 | Spec: kustomizev1.KustomizationSpec{ 37 | SourceRef: kustomizev1.CrossNamespaceSourceReference{}, 38 | }, 39 | }, 40 | expectedGitRepoNamespace: "kustomization-namespace", 41 | }, 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | gitRepoNamespace := getGitRepoNamespace(tt.kustomization) 46 | assert.Equal(t, gitRepoNamespace, tt.expectedGitRepoNamespace) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Christian Hernandez christian@chernand.io 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | "github.com/spf13/viper" 25 | ) 26 | 27 | // set up the global config file 28 | var cfgFile string 29 | 30 | // rootCmd represents the base command when called without any subcommands 31 | var rootCmd = &cobra.Command{ 32 | Use: "mta", 33 | Version: "v0.0.9", 34 | Short: "This commands turns Flux Kustomizations and HelmReleases into Argo CD Applications", 35 | Long: `This is a migration tool that helps you move your Flux Kustomizations and HelmReleases 36 | into an Argo CD ApplicationSet or Application. 37 | 38 | Kustomization example: 39 | 40 | mta kustomization --name=mykustomization --namespace=flux-system | kubectl apply -n argocd -f - 41 | 42 | HelmRelease example: 43 | 44 | mta helmrelease --name=myhelmrelease --namespace=flux-system | kubectl apply -n argocd -f - 45 | 46 | This utilty exports the named Kustomization or HelmRelease and the source Git repo or Helm repo and 47 | creates a manifests to stdout, which you can pipe into an apply command 48 | with kubectl.`, 49 | } 50 | 51 | // Execute adds all child commands to the root command and sets flags appropriately. 52 | // This is called by main.main(). It only needs to happen once to the rootCmd. 53 | func Execute() { 54 | err := rootCmd.Execute() 55 | if err != nil { 56 | os.Exit(1) 57 | } 58 | } 59 | 60 | func init() { 61 | cobra.OnInitialize(initConfig) 62 | 63 | // Here you will define your flags and configuration settings. 64 | // Cobra supports persistent flags, which, if defined here, 65 | // will be global for your application. 66 | 67 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mta.yaml)") 68 | 69 | rootCmd.PersistentFlags().String("kubeconfig", "", "Path to the kubeconfig file to use (if not the standard one).") 70 | rootCmd.PersistentFlags().String("name", "", "Name of Kustomization or HelmRelease to export") 71 | rootCmd.PersistentFlags().String("namespace", "flux-system", "Namespace of where the Kustomization or HelmRelease is") 72 | rootCmd.PersistentFlags().String("argocd-namespace", "argocd", "Namespace where Argo CD is installed") 73 | 74 | // Cobra also supports local flags, which will only run 75 | // when this action is called directly. 76 | //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 77 | } 78 | 79 | // initConfig reads in config file and ENV variables if set. 80 | func initConfig() { 81 | if cfgFile != "" { 82 | // Use config file from the flag. 83 | viper.SetConfigFile(cfgFile) 84 | } else { 85 | // Find home directory. 86 | home, err := os.UserHomeDir() 87 | cobra.CheckErr(err) 88 | 89 | // Search config in home directory with name ".mta" (without extension). 90 | viper.AddConfigPath(home) 91 | viper.SetConfigType("yaml") 92 | viper.SetConfigName(".mta") 93 | } 94 | 95 | viper.AutomaticEnv() // read in environment variables that match 96 | 97 | // If a config file is found, read it in. 98 | if err := viper.ReadInConfig(); err == nil { 99 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cmd/scan.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Christian Hernandez 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "context" 20 | "os" 21 | 22 | "github.com/akuity/mta/pkg/argo" 23 | "github.com/akuity/mta/pkg/utils" 24 | argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 25 | fluxlog "github.com/fluxcd/flux2/pkg/log" 26 | helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" 27 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 28 | sourcev1 "github.com/fluxcd/source-controller/api/v1" 29 | "github.com/jedib0t/go-pretty/v6/table" 30 | "github.com/manifoldco/promptui" 31 | log "github.com/sirupsen/logrus" 32 | "github.com/spf13/cobra" 33 | corev1 "k8s.io/api/core/v1" 34 | "k8s.io/apimachinery/pkg/runtime" 35 | client "sigs.k8s.io/controller-runtime/pkg/client" 36 | ) 37 | 38 | // scanCmd represents the scan command 39 | var scanCmd = &cobra.Command{ 40 | Use: "scan", 41 | Short: "Looks for all HelmReleases and Kustomizations", 42 | Long: `Looks for HelmReleases and Kustomizations in the cluster and 43 | displays the results. 44 | `, 45 | Run: func(cmd *cobra.Command, args []string) { 46 | // Get excluded-dirs from the cli 47 | exd, err := cmd.Flags().GetStringSlice("exclude-dirs") 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | // Set up the default context 53 | ctx := context.TODO() 54 | 55 | // Get the Kubeconfig to use 56 | kubeConfig, err := cmd.Flags().GetString("kubeconfig") 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | // Get automigrate option from the cli 62 | autoMigrate, err := cmd.Flags().GetBool("auto-migrate") 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | // Get automigrate option from the cli 67 | confirmMigrate, err := cmd.Flags().GetBool("confirm") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | // Get the Argo CD namespace in case of auto-migrate 73 | argoCDNamespace, err := cmd.Flags().GetString("argocd-namespace") 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | 78 | // Set up the schema because HelmRelease and Kustomization are CRDs 79 | kScheme := runtime.NewScheme() 80 | kustomizev1.AddToScheme(kScheme) 81 | sourcev1.AddToScheme(kScheme) 82 | helmv2.AddToScheme(kScheme) 83 | sourcev1.AddToScheme(kScheme) 84 | corev1.AddToScheme(kScheme) 85 | argov1alpha1.AddToScheme(kScheme) 86 | 87 | // create rest config using the kubeconfig file. 88 | restConfig, err := utils.NewRestConfig(kubeConfig) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | // Create a new client based on the restconfig and scheme 94 | k, err := client.New(restConfig, client.Options{ 95 | Scheme: kScheme, 96 | }) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | // Get all Helm Releases in the cluster 102 | helmReleaseList := &helmv2.HelmReleaseList{} 103 | if err = k.List(ctx, helmReleaseList); err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | // Get all Kustomizations in the cluster 108 | kustomizationList := &kustomizev1.KustomizationList{} 109 | if err = k.List(ctx, kustomizationList); err != nil { 110 | log.Fatal(err) 111 | } 112 | 113 | // Automigrate if the flag is set, otherwise just display the table 114 | if autoMigrate { 115 | // Prompt user to confirm migration 116 | if !confirmMigrate { 117 | prompt := promptui.Prompt{ 118 | Label: "Are you sure you want to migrate to Argo CD and uninstall Flux?", 119 | IsConfirm: true, 120 | } 121 | 122 | _, err := prompt.Run() 123 | 124 | if err != nil { 125 | log.Info("Automigration Cancelled") 126 | os.Exit(0) 127 | } 128 | } else { 129 | // Confirmation of migration has been confirmed 130 | log.Info("Auto-migration confirmed") 131 | } 132 | 133 | // Check if Argo CD is installed/running 134 | if !argo.IsArgoRunning(k, argoCDNamespace) { 135 | log.Fatal("Argo CD is not installed or running") 136 | } 137 | 138 | // Migrate Kustomizations 139 | for _, kl := range kustomizationList.Items { 140 | log.Info("Migrating Kustomization ", kl.Name) 141 | if err := utils.MigrateKustomizationToApplicationSet(k, ctx, argoCDNamespace, kl, exd); err != nil { 142 | log.Fatal(err) 143 | } 144 | } 145 | 146 | // Migrate HelmReleases 147 | for _, hl := range helmReleaseList.Items { 148 | log.Info("Migrating HelmRelease ", hl.Name) 149 | if err := utils.MigrateHelmReleaseToApplication(k, ctx, argoCDNamespace, hl); err != nil { 150 | log.Fatal(err) 151 | } 152 | } 153 | 154 | // Once we're done, we can uninstall Flux 155 | log.Info("Uninstalling Flux") 156 | if err := utils.FluxCleanUp(k, ctx, fluxlog.NopLogger{}, "flux-system"); err != nil { 157 | log.Fatal(err) 158 | } 159 | } else { 160 | 161 | // Set up table 162 | t := table.NewWriter() 163 | t.SetOutputMirror(os.Stdout) 164 | t.AppendHeader(table.Row{"Kind", "Name", "Namespace", "Status"}) 165 | 166 | // Add all Helm Releases to the table 167 | for _, hr := range helmReleaseList.Items { 168 | t.AppendRow(table.Row{hr.Kind, hr.Name, hr.Namespace, utils.TruncMsg(hr.Status.Conditions[0].Message)}) 169 | } 170 | 171 | // Add a separotor to the table 172 | t.AppendSeparator() 173 | 174 | // Add all Kustomizations to the table 175 | for _, k := range kustomizationList.Items { 176 | t.AppendRow(table.Row{k.Kind, k.Name, k.Namespace, utils.TruncMsg(k.Status.Conditions[0].Message)}) 177 | } 178 | 179 | //Render the table to the console 180 | t.SetStyle(table.StyleLight) 181 | t.Render() 182 | 183 | } 184 | 185 | }, 186 | } 187 | 188 | func init() { 189 | rootCmd.AddCommand(scanCmd) 190 | 191 | scanCmd.Flags().Bool("auto-migrate", false, "Migrate HelmReleases and Kustomizations to Argo CD and uninstalls Flux") 192 | scanCmd.Flags().Bool("confirm", false, "Confirm migraton to Argo CD and uninstalls Flux") 193 | scanCmd.Flags().StringSlice("exclude-dirs", []string{}, "Additional Directories (besides flux-system) to exclude from the GitDir generator. Can be single or comma separated") 194 | } 195 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Christian Hernandez christian@chernand.io 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package cmd 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | // versionCmd represents the version command 26 | var versionCmd = &cobra.Command{ 27 | Use: "version", 28 | Short: "Displays version.", 29 | Long: `This command will display the version of the CLI in json format`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | versionMap := map[string]string{rootCmd.Use: rootCmd.Version} 32 | versionJson, _ := json.Marshal(versionMap) 33 | fmt.Println(string(versionJson)) 34 | }, 35 | } 36 | 37 | func init() { 38 | rootCmd.AddCommand(versionCmd) 39 | } 40 | -------------------------------------------------------------------------------- /examples/helm/README.md: -------------------------------------------------------------------------------- 1 | The helm directory contains valid manifest files each of which 2 | creates a `HelmRepository` and `HelmRelease` resource that you can 3 | apply to your cluster and see how `mta` helps in converting those resources 4 | into compatible Argo CD CRs. 5 | 6 | ## How to use mta 7 | 8 | ### Example 1: 9 | 10 | In this example, we are going to use the `manifest-1.yaml` for reference. This manifest simply creates 11 | a `HelmRepository` resource and `HelmRelease` resource in the `default` namespace. The `HelmRelease` 12 | resource simply references a resource of kind `HelmRepository` and name `podinfo`. 13 | 14 | Once you've applied the manifest: 15 | 16 | 1. Run the `mta scan` command to look for all the Kustomization and Helmreleases resources 17 | ``` 18 | ┌───────────────┬─────────┬───────────┬────────────────────────────────────────────────────────────────────────┐ 19 | │ KIND │ NAME │ NAMESPACE │ STATUS │ 20 | ├───────────────┼─────────┼───────────┼────────────────────────────────────────────────────────────────────────┤ 21 | │ HelmRelease │ podinfo │ default │ Fulfilling prerequisites │ 22 | └───────────────┴─────────┴───────────┴────────────────────────────────────────────────────────────────────────┘ 23 | ``` 24 | 25 | 2. Convert the helmrelease resource into Argo CD compatible CR using the following command: 26 | ``` 27 | mta helmrelease --name podinfo --namespace default 28 | ``` 29 | This will generate valid Argo CD comptaible CRs for you to apply to the cluster. 30 | 31 | 32 | ### Example 2: 33 | 34 | In this example, we are going to use the `manifest-2.yaml` for reference. This manifest simply creates 35 | a `HelmRepository` resource in the `helm` namespace and `HelmRelease` resource in the `default` namespace. 36 | The `HelmRelease` resource simply references a `HelmRepository` named `helmrepo` that we created above 37 | in the `helm` namespace. 38 | 39 | Once you've applied the manifest: 40 | 41 | 1. Run the `mta scan` command to look for all the Kustomization and HelmRelease resources 42 | ``` 43 | ┌───────────────┬──────────────────┬───────────┬────────────────────────────────────────────────────────────────────────┐ 44 | │ KIND │ NAME │ NAMESPACE │ STATUS │ 45 | ├───────────────┼──────────────────┼───────────┼────────────────────────────────────────────────────────────────────────┤ 46 | │ HelmRelease │ helmrelease │ default │ Applied revision: master@sha1:b99bf8c252d47db1cccfb6546aec650679645e61 │ 47 | └───────────────┴──────────────────┴───────────┴────────────────────────────────────────────────────────────────────────┘ 48 | ``` 49 | 50 | 2. Convert the HelmRelease resource into Argo CD compatible CRs using the following command: 51 | ``` 52 | mta helmrelease --name helmrelease --namespace default 53 | ``` 54 | This will generate valid Argo CD comptaible CRs for you to apply to the cluster. 55 | 56 | ### Troubleshooting: 57 | 1. If you ever come across the following error message when converting to Argo CD compatible CRs 58 | 59 | ``` 60 | FATA[0000] helmrepositories.source.toolkit.fluxcd.io "" not found 61 | ``` 62 | 63 | Ensure that the `HelmRepository` that your `HelmRelease` resource references to exists in the namespace 64 | you've specified in the `sourceRef` field. If you've not specified a namespace in the `sourceRef` field, `mta` will search 65 | for the `HelmRepository` in `HelmRelease` resource namespace. For more information, refer to the: 66 | 67 | - [Helm Reference docs](https://fluxcd.io/flux/components/helm/helmreleases/). 68 | - [Helm API Reference docs](https://fluxcd.io/flux/components/helm/api/v2/) 69 | 70 | 2. If you don't specify the `--namespace` flag to `mta helmrelease` command , mta will try to look for a `HelmRelease` resource in the `flux-system` namespace. 71 | Hence, If you get the following error message without specifying the namespace flag 72 | 73 | ``` 74 | FATA[0000] helmreleases.helm.toolkit.fluxcd.io "" not found 75 | ``` 76 | 77 | Ensure that the `HelmRelease` resource exists in the `flux-system` namespace. 78 | -------------------------------------------------------------------------------- /examples/helm/manifest-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: HelmRepository 3 | metadata: 4 | name: podinfo 5 | namespace: default 6 | spec: 7 | interval: 1m 8 | url: https://stefanprodan.github.io/podinfo 9 | secretRef: 10 | name: example-user 11 | --- 12 | apiVersion: v1 13 | kind: Secret 14 | metadata: 15 | name: example-user 16 | namespace: default 17 | type: Opaque 18 | stringData: 19 | username: example 20 | password: "123456" 21 | --- 22 | apiVersion: helm.toolkit.fluxcd.io/v2 23 | kind: HelmRelease 24 | metadata: 25 | name: podinfo 26 | namespace: default 27 | spec: 28 | interval: 1m 29 | chart: 30 | spec: 31 | chart: podinfo 32 | version: '4.0.1' 33 | sourceRef: 34 | kind: HelmRepository 35 | name: podinfo 36 | install: 37 | createNamespace: true 38 | upgrade: 39 | remediation: 40 | retries: 3 41 | values: 42 | replicaCount: 2 43 | -------------------------------------------------------------------------------- /examples/helm/manifest-2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: HelmRepository 3 | metadata: 4 | name: helmrepo 5 | namespace: helm 6 | spec: 7 | interval: 1m 8 | url: https://stefanprodan.github.io/podinfo 9 | secretRef: 10 | name: example-user 11 | --- 12 | apiVersion: v1 13 | kind: Secret 14 | metadata: 15 | name: example-user 16 | namespace: default 17 | type: Opaque 18 | stringData: 19 | username: example 20 | password: "123456" 21 | --- 22 | apiVersion: helm.toolkit.fluxcd.io/v2 23 | kind: HelmRelease 24 | metadata: 25 | name: helmrelease 26 | namespace: default 27 | spec: 28 | interval: 1m 29 | chart: 30 | spec: 31 | chart: podinfo 32 | version: '4.0.1' 33 | sourceRef: 34 | kind: HelmRepository 35 | name: helmrepo 36 | install: 37 | createNamespace: true 38 | upgrade: 39 | remediation: 40 | retries: 3 41 | values: 42 | replicaCount: 2 43 | -------------------------------------------------------------------------------- /examples/kustomization/README.md: -------------------------------------------------------------------------------- 1 | The kustomization directory contains valid manifest files each of which 2 | creates a `GitRepository` and `Kustomization` resource that you can 3 | apply to your cluster and see how `mta` helps in converting those resources 4 | into compatible Argo CD CRs. 5 | 6 | ## How to use mta 7 | 8 | ### Example 1: 9 | 10 | In this example, we are going to use the `manifest-1.yaml` for reference. This manifest simply creates 11 | a `GitRepository` resource and `Kustomization` resource in the `default` namespace. The `Kustomization` 12 | resource simply references a resource of kind `GitRepository` and name `podinfo`. 13 | 14 | Once you've applied the manifest: 15 | 16 | 1. Run the `mta scan` command to look for all the Kustomization and HelmRelease resources 17 | ``` 18 | ┌───────────────┬─────────┬───────────┬────────────────────────────────────────────────────────────────────────┐ 19 | │ KIND │ NAME │ NAMESPACE │ STATUS │ 20 | ├───────────────┼─────────┼───────────┼────────────────────────────────────────────────────────────────────────┤ 21 | │ Kustomization │ podinfo │ default │ Applied revision: master@sha1:b99bf8c252d47db1cccfb6546aec650679645e61 │ 22 | └───────────────┴─────────┴───────────┴────────────────────────────────────────────────────────────────────────┘ 23 | ``` 24 | 25 | 2. Convert the Kustomization resource into Argo CD compatible CR using the following command: 26 | ``` 27 | mta kustomization --name podinfo --namespace default 28 | ``` 29 | This will generate valid Argo CD comptaible CRs for you to apply to the cluster. 30 | 31 | 32 | ### Example 2: 33 | 34 | In this example, we are going to use the `manifest-2.yaml` for reference. This manifest simply creates 35 | a `GitRepository` resource in the `git` namespace and `Kustomization` resource in the `default` namespace. 36 | The `Kustomization` resource simply references a `GitRepository` named `git-repository` that we created above 37 | in the `git` namespace. 38 | 39 | Once you've applied the manifest: 40 | 41 | 1. Run the `mta scan` command to look for all the Kustomization and Helmreleases resources 42 | ``` 43 | ┌───────────────┬──────────────────┬───────────┬────────────────────────────────────────────────────────────────────────┐ 44 | │ KIND │ NAME │ NAMESPACE │ STATUS │ 45 | ├───────────────┼──────────────────┼───────────┼────────────────────────────────────────────────────────────────────────┤ 46 | │ Kustomization │ my-kustomization │ default │ Applied revision: master@sha1:b99bf8c252d47db1cccfb6546aec650679645e61 │ 47 | └───────────────┴──────────────────┴───────────┴────────────────────────────────────────────────────────────────────────┘ 48 | ``` 49 | 50 | 2. Convert the Kustomization resource into Argo CD compatible CRs using the following command: 51 | ``` 52 | mta kustomization --name my-kustomization --namespace default 53 | ``` 54 | This will generate valid Argo CD comptaible CRs for you to apply to the cluster. 55 | 56 | ### Troubleshooting: 57 | 1. If you ever come across the following error message when converting to Argo CD compatible CRs 58 | 59 | ``` 60 | FATA[0000] gitrepositories.source.toolkit.fluxcd.io "" not found 61 | ``` 62 | 63 | Ensure that the `GitRepository` that your `Kustomization` resources references to exists in the namespace 64 | you've specified in the `sourceRef` field. If you've not specified a namespace in the `sourceRef` namespace, `mta` will search 65 | for the `GitRepository` in `Kustomization` resource namespace. For more information, refer to the: 66 | 67 | - [Kustomization Reference docs](https://fluxcd.io/flux/components/kustomize/kustomizations/). 68 | - [Kustomization API Reference docs](https://fluxcd.io/flux/components/kustomize/api/v1/) 69 | 70 | 2. If you don't specify the `--namespace` flag to `mta kustomization` command, mta will try to look for a `Kustomization` resource in the `flux-system` namespace. 71 | Hence, If you get the following error message without specifying the namespace flag 72 | 73 | ``` 74 | FATA[0000] kustomizations.kustomize.toolkit.fluxcd.io "" not found 75 | ``` 76 | 77 | Ensure that the `Kustomization` resource exists in the `flux-system` namespace. 78 | -------------------------------------------------------------------------------- /examples/kustomization/manifest-1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: GitRepository 3 | metadata: 4 | name: podinfo 5 | namespace: default 6 | spec: 7 | interval: 5m 8 | url: https://github.com/stefanprodan/podinfo 9 | ref: 10 | branch: master 11 | --- 12 | apiVersion: kustomize.toolkit.fluxcd.io/v1 13 | kind: Kustomization 14 | metadata: 15 | name: podinfo 16 | namespace: default 17 | spec: 18 | interval: 10m 19 | targetNamespace: default 20 | sourceRef: 21 | kind: GitRepository 22 | name: podinfo 23 | path: "./kustomize" 24 | prune: true 25 | timeout: 1m -------------------------------------------------------------------------------- /examples/kustomization/manifest-2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: source.toolkit.fluxcd.io/v1 2 | kind: GitRepository 3 | metadata: 4 | name: git-repository 5 | namespace: git 6 | spec: 7 | interval: 5m 8 | url: https://github.com/stefanprodan/podinfo 9 | ref: 10 | branch: master 11 | --- 12 | apiVersion: kustomize.toolkit.fluxcd.io/v1 13 | kind: Kustomization 14 | metadata: 15 | name: my-kustomization 16 | namespace: default 17 | spec: 18 | interval: 10m 19 | targetNamespace: default 20 | sourceRef: 21 | kind: GitRepository 22 | name: git-repository 23 | namespace: git 24 | path: "./kustomize" 25 | prune: true 26 | timeout: 1m -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/akuity/mta 2 | 3 | go 1.21.3 4 | 5 | replace ( 6 | github.com/chai2010/gettext-go => github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 7 | k8s.io/api => k8s.io/api v0.24.2 8 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.24.2 9 | k8s.io/apimachinery => k8s.io/apimachinery v0.24.2 10 | k8s.io/apiserver => k8s.io/apiserver v0.24.2 11 | k8s.io/cli-runtime => k8s.io/cli-runtime v0.24.2 12 | k8s.io/client-go => k8s.io/client-go v0.24.2 13 | k8s.io/cloud-provider => k8s.io/cloud-provider v0.24.2 14 | k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.24.2 15 | k8s.io/code-generator => k8s.io/code-generator v0.24.2 16 | k8s.io/component-base => k8s.io/component-base v0.24.2 17 | k8s.io/component-helpers => k8s.io/component-helpers v0.24.2 18 | k8s.io/controller-manager => k8s.io/controller-manager v0.24.2 19 | k8s.io/cri-api => k8s.io/cri-api v0.24.2 20 | k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.24.2 21 | k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.24.2 22 | k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.24.2 23 | k8s.io/kube-proxy => k8s.io/kube-proxy v0.24.2 24 | k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.24.2 25 | k8s.io/kubectl => k8s.io/kubectl v0.24.2 26 | k8s.io/kubelet => k8s.io/kubelet v0.24.2 27 | k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.24.2 28 | k8s.io/metrics => k8s.io/metrics v0.24.2 29 | k8s.io/mount-utils => k8s.io/mount-utils v0.24.2 30 | k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 31 | k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 32 | ) 33 | 34 | require ( 35 | github.com/argoproj/argo-cd/v2 v2.7.2 36 | github.com/fluxcd/flux2 v0.41.2 37 | github.com/fluxcd/helm-controller/api v0.33.0 38 | github.com/fluxcd/kustomize-controller/api v1.1.1 39 | github.com/fluxcd/source-controller/api v1.1.2 40 | github.com/jedib0t/go-pretty/v6 v6.4.2 41 | github.com/manifoldco/promptui v0.9.0 42 | github.com/sirupsen/logrus v1.9.0 43 | github.com/spf13/cobra v1.7.0 44 | github.com/spf13/viper v1.14.0 45 | k8s.io/api v0.28.3 46 | k8s.io/apimachinery v0.28.4 47 | k8s.io/cli-runtime v0.26.2 48 | k8s.io/client-go v0.28.3 49 | sigs.k8s.io/controller-runtime v0.15.3 50 | sigs.k8s.io/yaml v1.4.0 51 | ) 52 | 53 | require ( 54 | cloud.google.com/go/compute v1.19.0 // indirect 55 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 56 | github.com/chai2010/gettext-go v1.0.2 // indirect 57 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 58 | github.com/cloudflare/circl v1.3.2 // indirect 59 | github.com/cyphar/filepath-securejoin v0.2.3 // indirect 60 | github.com/fluxcd/image-automation-controller/api v0.31.0 // indirect 61 | github.com/fluxcd/image-reflector-controller/api v0.26.1 // indirect 62 | github.com/fluxcd/notification-controller/api v0.33.0 // indirect 63 | github.com/go-redis/cache/v9 v9.0.0 // indirect 64 | github.com/google/go-github/v45 v45.2.0 // indirect 65 | github.com/pjbgf/sha1cd v0.3.0 // indirect 66 | github.com/redis/go-redis/v9 v9.0.2 // indirect 67 | github.com/robfig/cron/v3 v3.0.1 // indirect 68 | github.com/skeema/knownhosts v1.1.0 // indirect 69 | golang.org/x/mod v0.10.0 // indirect 70 | golang.org/x/tools v0.9.3 // indirect 71 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect 72 | gopkg.in/yaml.v2 v2.4.0 // indirect 73 | ) 74 | 75 | require ( 76 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 77 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 78 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 79 | github.com/Microsoft/go-winio v0.6.0 // indirect 80 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 81 | github.com/acomagu/bufpipe v1.0.4 // indirect 82 | github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd // indirect 83 | github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d // indirect 84 | github.com/bombsimon/logrusr/v2 v2.0.1 // indirect 85 | github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 // indirect 86 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 87 | //github.com/chai2010/gettext-go v1.0.1 // indirect 88 | //github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect 89 | github.com/davecgh/go-spew v1.1.1 // indirect 90 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 91 | github.com/docker/distribution v2.8.1+incompatible // indirect 92 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 93 | github.com/emirpasic/gods v1.18.1 // indirect 94 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 95 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 96 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 97 | github.com/fatih/camelcase v1.0.0 // indirect 98 | github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect 99 | github.com/fluxcd/pkg/apis/kustomize v1.2.0 // indirect 100 | github.com/fluxcd/pkg/apis/meta v1.2.0 // indirect 101 | github.com/fsnotify/fsnotify v1.6.0 // indirect 102 | github.com/fvbommel/sortorder v1.0.1 // indirect 103 | github.com/ghodss/yaml v1.0.0 // indirect 104 | github.com/go-errors/errors v1.4.2 // indirect 105 | github.com/go-git/gcfg v1.5.0 // indirect 106 | github.com/go-git/go-billy/v5 v5.4.1 // indirect 107 | github.com/go-git/go-git/v5 v5.6.1 // indirect 108 | github.com/go-logr/logr v1.3.0 // indirect 109 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 110 | github.com/go-openapi/jsonreference v0.20.2 // indirect 111 | github.com/go-openapi/swag v0.22.3 // indirect 112 | github.com/gobwas/glob v0.2.3 // indirect 113 | github.com/gogo/protobuf v1.3.2 // indirect 114 | github.com/golang-jwt/jwt/v4 v4.4.3 // indirect 115 | github.com/golang/protobuf v1.5.3 // indirect 116 | github.com/google/btree v1.1.2 // indirect 117 | github.com/google/gnostic v0.6.9 // indirect 118 | github.com/google/go-cmp v0.5.9 // indirect 119 | github.com/google/go-querystring v1.1.0 // indirect 120 | github.com/google/gofuzz v1.2.0 // indirect 121 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 122 | github.com/google/uuid v1.3.0 // indirect 123 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 124 | github.com/hashicorp/hcl v1.0.0 // indirect 125 | github.com/imdario/mergo v0.3.13 // indirect 126 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 127 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 128 | github.com/jonboulle/clockwork v0.2.2 // indirect 129 | github.com/josharian/intern v1.0.0 // indirect 130 | github.com/json-iterator/go v1.1.12 // indirect 131 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 132 | github.com/kevinburke/ssh_config v1.2.0 // indirect 133 | github.com/klauspost/compress v1.15.11 // indirect 134 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 135 | github.com/magiconair/properties v1.8.6 136 | github.com/mailru/easyjson v0.7.7 // indirect 137 | github.com/mattn/go-runewidth v0.0.14 // indirect 138 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 139 | github.com/mitchellh/mapstructure v1.5.0 // indirect 140 | github.com/moby/spdystream v0.2.0 // indirect 141 | github.com/moby/term v0.0.0-20221105221325-4eb28fa6025c // indirect 142 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 143 | github.com/modern-go/reflect2 v1.0.2 // indirect 144 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 145 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 146 | github.com/opencontainers/go-digest v1.0.0 // indirect 147 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 148 | github.com/pelletier/go-toml v1.9.5 // indirect 149 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 150 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 151 | github.com/pkg/errors v0.9.1 // indirect 152 | github.com/rivo/uniseg v0.2.0 // indirect 153 | github.com/russross/blackfriday v1.6.0 // indirect 154 | github.com/sergi/go-diff v1.3.1 // indirect 155 | github.com/spf13/afero v1.9.2 // indirect 156 | github.com/spf13/cast v1.5.0 // indirect 157 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 158 | github.com/spf13/pflag v1.0.5 // indirect 159 | github.com/subosito/gotenv v1.4.1 // indirect 160 | github.com/vmihailenco/go-tinylfu v0.2.2 // indirect 161 | github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect 162 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 163 | github.com/xanzy/ssh-agent v0.3.3 // indirect 164 | github.com/xlab/treeprint v1.1.0 // indirect 165 | go.starlark.net v0.0.0-20221028183056-acb66ad56dd2 // indirect 166 | golang.org/x/crypto v0.16.0 // indirect 167 | golang.org/x/net v0.19.0 // indirect 168 | golang.org/x/oauth2 v0.8.0 // indirect 169 | golang.org/x/sync v0.2.0 // indirect 170 | golang.org/x/sys v0.15.0 // indirect 171 | golang.org/x/term v0.15.0 // indirect 172 | golang.org/x/text v0.14.0 // indirect 173 | golang.org/x/time v0.3.0 // indirect 174 | google.golang.org/appengine v1.6.7 // indirect 175 | google.golang.org/grpc v1.54.0 // indirect 176 | google.golang.org/protobuf v1.30.0 // indirect 177 | gopkg.in/inf.v0 v0.9.1 // indirect 178 | gopkg.in/ini.v1 v1.67.0 // indirect 179 | gopkg.in/warnings.v0 v0.1.2 // indirect 180 | gopkg.in/yaml.v3 v3.0.1 // indirect 181 | k8s.io/apiextensions-apiserver v0.28.4 // indirect 182 | k8s.io/apiserver v0.28.3 // indirect 183 | k8s.io/component-base v0.28.3 // indirect 184 | k8s.io/component-helpers v0.24.2 // indirect 185 | k8s.io/klog/v2 v2.110.1 // indirect 186 | k8s.io/kube-aggregator v0.24.2 // indirect 187 | k8s.io/kube-openapi v0.0.0-20230525220651-2546d827e515 // indirect 188 | k8s.io/kubectl v0.26.2 // indirect 189 | k8s.io/kubernetes v1.24.2 // indirect 190 | k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect 191 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 192 | sigs.k8s.io/kustomize/api v0.12.1 // indirect 193 | sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect 194 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 195 | ) 196 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Christian Hernandez christian@chernand.io 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import "github.com/akuity/mta/cmd" 19 | 20 | func main() { 21 | cmd.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/argo/argo.go: -------------------------------------------------------------------------------- 1 | package argo 2 | 3 | import ( 4 | "context" 5 | 6 | v1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 7 | apiv1 "k8s.io/api/core/v1" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/types" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | // ArgoCdGitApplicationSet is a struct that holds the ArgoCD Git ApplicationSet 14 | // TODO: Make a Generic "ApplicationSet" struct that can be used generically (i.e. specify your generator) 15 | type GitDirApplicationSet struct { 16 | Namespace string 17 | GitRepoURL string 18 | GitRepoRevision string 19 | GitIncludeDir string 20 | GitExcludeDir []string 21 | AppName string 22 | AppProject string 23 | AppRepoURL string 24 | AppTargetRevision string 25 | AppPath string 26 | AppDestinationServer string 27 | AppDestinationNamespace string 28 | SSHPrivateKey string 29 | GitOpsRepo string 30 | } 31 | 32 | // ArgoCdApplication is a struct that holds the ArgoCD Application 33 | type ArgoCdHelmApplication struct { 34 | Name string 35 | Namespace string 36 | DestinationNamespace string 37 | DestinationServer string 38 | Project string 39 | HelmChart string 40 | HelmRepo string 41 | HelmTargetRevision string 42 | HelmValues string 43 | HelmCreateNamespace string 44 | } 45 | 46 | // GenArgoCdApplication generates an ArgoCD Application 47 | func GenArgoCdHelmApplication(app ArgoCdHelmApplication) (*v1alpha1.Application, error) { 48 | // Some Defaults 49 | // TODO: Make these configurable 50 | aSPAutomated := v1alpha1.SyncPolicyAutomated{Prune: true, SelfHeal: true} 51 | aSyncOptions := v1alpha1.SyncOptions{"CreateNamespace=" + app.HelmCreateNamespace, "Validate=false"} 52 | 53 | // Create Empty Application 54 | a := &v1alpha1.Application{} 55 | 56 | // Set GVK scheme 57 | a.SetGroupVersionKind(v1alpha1.SchemeGroupVersion.WithKind("Application")) 58 | a.SetName(app.Name) 59 | a.SetNamespace(app.Namespace) 60 | a.Spec = v1alpha1.ApplicationSpec{ 61 | Project: app.Project, 62 | Source: &v1alpha1.ApplicationSource{ 63 | Chart: app.HelmChart, 64 | RepoURL: app.HelmRepo, 65 | TargetRevision: app.HelmTargetRevision, 66 | Helm: &v1alpha1.ApplicationSourceHelm{ 67 | Values: app.HelmValues, 68 | }, 69 | }, 70 | Destination: v1alpha1.ApplicationDestination{ 71 | Namespace: app.DestinationNamespace, 72 | Server: app.DestinationServer, 73 | }, 74 | SyncPolicy: &v1alpha1.SyncPolicy{ 75 | Automated: &aSPAutomated, 76 | SyncOptions: aSyncOptions, 77 | }, 78 | } 79 | 80 | // Return the application def 81 | return a, nil 82 | } 83 | 84 | // GenGitDirApplicationSet generates an ArgoCD Git Directory ApplicationSet that 85 | func GenGitDirAppSet(appSet GitDirApplicationSet) (*v1alpha1.ApplicationSet, error) { 86 | // Some Defaults 87 | // TODO: Make these configurable 88 | var TargetNamespace string 89 | asName := "mta-migration" 90 | asSyncOptions := v1alpha1.SyncOptions{"CreateNamespace=true", "Validate=false"} 91 | asSPAutomated := v1alpha1.SyncPolicyAutomated{Prune: true, SelfHeal: true} 92 | asRetry := v1alpha1.RetryStrategy{Limit: 5, Backoff: &v1alpha1.Backoff{Duration: "5s", Factor: func(i int64) *int64 { return &i }(2), MaxDuration: "3m"}} 93 | 94 | // Set the Target Namespace to "default" if it's not set 95 | if appSet.AppDestinationNamespace == "" { 96 | TargetNamespace = "default" 97 | } else { 98 | TargetNamespace = appSet.AppDestinationNamespace 99 | } 100 | 101 | // Create Empty ApplicationSet 102 | as := &v1alpha1.ApplicationSet{} 103 | 104 | // Set GVK scheme 105 | as.SetGroupVersionKind(v1alpha1.SchemeGroupVersion.WithKind("ApplicationSet")) 106 | 107 | as.SetName(asName) 108 | as.SetNamespace(appSet.Namespace) 109 | // 110 | as.Spec.Generators = []v1alpha1.ApplicationSetGenerator{ 111 | { 112 | Git: &v1alpha1.GitGenerator{ 113 | RepoURL: appSet.GitRepoURL, 114 | Revision: appSet.GitRepoRevision, 115 | Directories: []v1alpha1.GitDirectoryGeneratorItem{ 116 | {Path: appSet.GitIncludeDir}, 117 | }, 118 | //Template: v1alpha1.ApplicationSetTemplate{}, 119 | }, 120 | }, 121 | } 122 | 123 | // Append any excluded directories 124 | for _, d := range appSet.GitExcludeDir { 125 | as.Spec.Generators[0].Git.Directories = append(as.Spec.Generators[0].Git.Directories, v1alpha1.GitDirectoryGeneratorItem{Path: d, Exclude: true}) 126 | } 127 | 128 | // Reset the Git Template spec because we aren't using it 129 | as.Spec.Generators[0].Git.Template.Reset() 130 | 131 | // Set up the Application template Spec 132 | as.Spec.Template = v1alpha1.ApplicationSetTemplate{ 133 | ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ 134 | Name: appSet.AppName, 135 | }, 136 | Spec: v1alpha1.ApplicationSpec{ 137 | Project: appSet.AppProject, 138 | SyncPolicy: &v1alpha1.SyncPolicy{ 139 | SyncOptions: asSyncOptions, 140 | Automated: &asSPAutomated, 141 | Retry: &asRetry, 142 | }, 143 | Source: &v1alpha1.ApplicationSource{ 144 | RepoURL: appSet.AppRepoURL, 145 | TargetRevision: appSet.AppTargetRevision, 146 | Path: appSet.AppPath, 147 | }, 148 | Destination: v1alpha1.ApplicationDestination{ 149 | Server: appSet.AppDestinationServer, 150 | Namespace: TargetNamespace, 151 | }, 152 | }, 153 | } 154 | 155 | // Return ApplicationSet 156 | return as, nil 157 | } 158 | 159 | // IsArgoRunning checks if ArgoCD is running. Best effort as it just checks to see if the namespace exists 160 | func IsArgoRunning(client client.Client, ns string) bool { 161 | // If we can't fine the namespace, return false 162 | namespaceobj := &apiv1.Namespace{} 163 | err := client.Get(context.Background(), types.NamespacedName{Name: ns}, namespaceobj) 164 | 165 | if err != nil && apierrors.IsNotFound(err) { 166 | return false 167 | } 168 | 169 | // If we're here we should be okay 170 | return true 171 | } 172 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/akuity/mta/pkg/argo" 11 | helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" 12 | yaml "sigs.k8s.io/yaml" 13 | 14 | "sigs.k8s.io/controller-runtime/pkg/client" 15 | 16 | "github.com/fluxcd/flux2/pkg/log" 17 | fluxuninstall "github.com/fluxcd/flux2/pkg/uninstall" 18 | kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" 19 | sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" 20 | apiv1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/types" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/tools/clientcmd" 25 | ) 26 | 27 | // MigrateKustomizationToApplicationSet migrates a Kustomization to an Argo CD ApplicationSet 28 | func MigrateKustomizationToApplicationSet(c client.Client, ctx context.Context, ans string, k kustomizev1.Kustomization, exd []string) error { 29 | // excludedDirs will be paths excluded by the gidir generator 30 | excludedDirs := exd 31 | 32 | // Get the GitRepository from the Kustomization 33 | // get the gitsource 34 | gitSource := &sourcev1.GitRepository{} 35 | err := c.Get(ctx, types.NamespacedName{Namespace: k.Namespace, Name: k.Name}, gitSource) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | //Get the secret holding the info we need 41 | //secret, err := _.CoreV1().Secrets(k.Namespace).Get(ctx, gitSource.Spec.SecretRef.Name, v1.GetOptions{}) 42 | secret := &apiv1.Secret{} 43 | err = c.Get(ctx, types.NamespacedName{Namespace: k.Namespace, Name: gitSource.Spec.SecretRef.Name}, secret) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | //Argo CD ApplicationSet is sensitive about how you give it paths in the Git Dir generator. We need to figure some things out 49 | var sourcePath string 50 | var sourcePathExclude string 51 | 52 | spl := strings.SplitAfter(k.Spec.Path, "./") 53 | 54 | if len(spl[1]) == 0 { 55 | sourcePath = `*` 56 | sourcePathExclude = "flux-system" 57 | } else { 58 | sourcePath = spl[1] + "/*" 59 | sourcePathExclude = spl[1] + "/flux-system" 60 | } 61 | 62 | // Add sourcePathExclude to the excludedDirs 63 | excludedDirs = append(excludedDirs, sourcePathExclude) 64 | 65 | // Generate the ApplicationSet manifest based on the struct 66 | applicationSet := argo.GitDirApplicationSet{ 67 | Namespace: ans, 68 | GitRepoURL: gitSource.Spec.URL, 69 | GitRepoRevision: gitSource.Spec.Reference.Branch, 70 | GitIncludeDir: sourcePath, 71 | GitExcludeDir: excludedDirs, 72 | AppName: "{{path.basename}}", 73 | AppProject: "default", 74 | AppRepoURL: gitSource.Spec.URL, 75 | AppTargetRevision: gitSource.Spec.Reference.Branch, 76 | AppPath: "{{path}}", 77 | AppDestinationServer: "https://kubernetes.default.svc", 78 | AppDestinationNamespace: k.Spec.TargetNamespace, 79 | SSHPrivateKey: string(secret.Data["identity"]), 80 | GitOpsRepo: gitSource.Spec.URL, 81 | } 82 | 83 | appset, err := argo.GenGitDirAppSet(applicationSet) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | // Generate the ApplicationSet Secret and set the GVK 89 | appsetSecret := GenK8SSecret(applicationSet) 90 | 91 | // Suspend Kustomization reconcilation 92 | if err := SuspendFluxObject(c, ctx, &k); err != nil { 93 | return err 94 | 95 | } 96 | 97 | // Suspend git repo reconcilation 98 | if err := SuspendFluxObject(c, ctx, gitSource); err != nil { 99 | return err 100 | } 101 | 102 | // Finally, create the Argo CD Application 103 | if err := CreateK8SObjects(c, ctx, appsetSecret, appset); err != nil { 104 | return err 105 | } 106 | 107 | // Delete the Kustomization 108 | if err := DeleteK8SObjects(c, ctx, &k); err != nil { 109 | return err 110 | } 111 | 112 | // Delete the GitRepository 113 | if err := DeleteK8SObjects(c, ctx, gitSource); err != nil { 114 | return err 115 | } 116 | 117 | // If we're here, it should have gone okay... 118 | return nil 119 | } 120 | 121 | // MigrateHelmReleaseToApplication migrates a HelmRelease to an Argo CD Application 122 | func MigrateHelmReleaseToApplication(c client.Client, ctx context.Context, ans string, h helmv2.HelmRelease) error { 123 | // Get the helmchart based on type, report if error 124 | helmRepo := &sourcev1.HelmRepository{} 125 | helmChart := &sourcev1.HelmChart{} 126 | err := c.Get(ctx, types.NamespacedName{Namespace: h.Namespace, Name: h.Spec.Chart.Spec.SourceRef.Name}, helmRepo) 127 | if err != nil { 128 | return err 129 | } 130 | err = c.Get(ctx, types.NamespacedName{Namespace: h.Namespace, Name: h.Namespace + "-" + h.Name}, helmChart) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | // Get the Values from the HelmRelease 136 | yaml, err := yaml.Marshal(h.Spec.Values) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | // Generate the Argo CD Helm Application 142 | helmApp := argo.ArgoCdHelmApplication{ 143 | Name: h.Spec.TargetNamespace + "-" + h.Name, 144 | Namespace: ans, 145 | DestinationNamespace: h.Spec.TargetNamespace, 146 | DestinationServer: "https://kubernetes.default.svc", 147 | Project: "default", 148 | HelmChart: h.Spec.Chart.Spec.Chart, 149 | HelmRepo: helmRepo.Spec.URL, 150 | HelmTargetRevision: h.Spec.Chart.Spec.Version, 151 | HelmValues: string(yaml), 152 | HelmCreateNamespace: strconv.FormatBool(h.Spec.Install.CreateNamespace), 153 | } 154 | 155 | helmArgoCdApp, err := argo.GenArgoCdHelmApplication(helmApp) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | // Suspend helm reconcilation 161 | if err := SuspendFluxObject(c, ctx, &h); err != nil { 162 | return err 163 | } 164 | 165 | // Suspend helm repo reconcilation 166 | if err := SuspendFluxObject(c, ctx, helmRepo); err != nil { 167 | return err 168 | } 169 | 170 | // Suspend helm repo reconcilation 171 | if err := SuspendFluxObject(c, ctx, helmChart); err != nil { 172 | return err 173 | } 174 | 175 | // Finally, create the Argo CD Application 176 | if err := CreateK8SObjects(c, ctx, helmArgoCdApp); err != nil { 177 | return err 178 | } 179 | 180 | // Delete the HelmRelease 181 | if err := DeleteK8SObjects(c, ctx, &h); err != nil { 182 | return err 183 | } 184 | 185 | // Delete the HelmRepository 186 | if err := DeleteK8SObjects(c, ctx, helmRepo); err != nil { 187 | return err 188 | } 189 | 190 | // Delete the HelmChart 191 | if err := DeleteK8SObjects(c, ctx, helmChart); err != nil { 192 | return err 193 | } 194 | 195 | // If we're here, it should have gone okay... 196 | return nil 197 | } 198 | 199 | // FluxCleanUp cleans up flux resources 200 | func FluxCleanUp(k client.Client, ctx context.Context, log log.Logger, ns string) error { 201 | // Set up the context with timeout 202 | cwt, cancel := context.WithTimeout(ctx, 5*time.Minute) 203 | defer cancel() 204 | 205 | //Set up the flux uninstall options 206 | // TODO: Maybe make these configurable 207 | uninstallFlags := struct { 208 | keepNamespace bool 209 | dryRun bool 210 | silent bool 211 | }{ 212 | keepNamespace: false, 213 | dryRun: false, 214 | silent: false, 215 | } 216 | 217 | // Uninstall the components 218 | if err := fluxuninstall.Components(cwt, log, k, ns, uninstallFlags.dryRun); err != nil { 219 | return err 220 | } 221 | 222 | // Uninstall the finalizers 223 | if err := fluxuninstall.Finalizers(cwt, log, k, uninstallFlags.dryRun); err != nil { 224 | return err 225 | } 226 | 227 | // Uninstall CRDS 228 | if err := fluxuninstall.CustomResourceDefinitions(cwt, log, k, uninstallFlags.dryRun); err != nil { 229 | return err 230 | } 231 | 232 | // Uninstall the namespace 233 | if err := fluxuninstall.Namespace(cwt, log, k, ns, uninstallFlags.dryRun); err != nil { 234 | return err 235 | } 236 | 237 | // If we're here, it should have gone okay... 238 | return nil 239 | } 240 | 241 | // SuspendFluxObject suspends Flux specific objects based on the schema passed in the client. 242 | func SuspendFluxObject(c client.Client, ctx context.Context, obj ...client.Object) error { 243 | // suspend the objects 244 | for _, o := range obj { 245 | if err := c.Patch(ctx, o, client.RawPatch(types.MergePatchType, []byte(`{"spec":{"suspend":true}}`))); err != nil { 246 | return err 247 | } 248 | } 249 | 250 | // If we're here, it should have gone okay... 251 | return nil 252 | } 253 | 254 | // CreateK8SObjects Creates Kubernetes Objects on the Cluster based on the schema passed in the client. 255 | func CreateK8SObjects(c client.Client, ctx context.Context, obj ...client.Object) error { 256 | // Migrate the objects 257 | for _, o := range obj { 258 | if err := c.Create(ctx, o); err != nil { 259 | return err 260 | } 261 | } 262 | 263 | // If we're here, it should have gone okay... 264 | return nil 265 | } 266 | 267 | // DeleteK8SObjects Deletes Kubernetes Objects on the Cluster based on the schema passed in the client. 268 | func DeleteK8SObjects(c client.Client, ctx context.Context, obj ...client.Object) error { 269 | // Migrate the objects 270 | for _, o := range obj { 271 | if err := c.Delete(ctx, o); err != nil { 272 | return err 273 | } 274 | } 275 | 276 | // If we're here, it should have gone okay... 277 | return nil 278 | } 279 | 280 | // GenK8SSecret generates a kubernetes secret using a clientset 281 | func GenK8SSecret(a argo.GitDirApplicationSet) *apiv1.Secret { 282 | // Some Defaults 283 | // TODO: Make these configurable 284 | sData := map[string]string{} 285 | sName := "mta-migration" 286 | sLabels := map[string]string{ 287 | "argocd.argoproj.io/secret-type": "repository", 288 | } 289 | 290 | sData = map[string]string{ 291 | "sshPrivateKey": a.SSHPrivateKey, 292 | "type": "git", 293 | "url": a.GitOpsRepo, 294 | } 295 | 296 | // Create the secret 297 | s := &apiv1.Secret{ 298 | ObjectMeta: metav1.ObjectMeta{ 299 | Name: sName, 300 | Namespace: a.Namespace, 301 | Labels: sLabels, 302 | }, 303 | Type: apiv1.SecretTypeOpaque, 304 | StringData: sData, 305 | } 306 | 307 | // set the gvk for the secret 308 | s.SetGroupVersionKind(apiv1.SchemeGroupVersion.WithKind("Secret")) 309 | 310 | // Return the secret 311 | return s 312 | 313 | } 314 | 315 | // NewRestClient returns a rest.Config 316 | func NewRestConfig(kubeConfigPath string) (*rest.Config, error) { 317 | if kubeConfigPath == "" { 318 | kubeConfigPath = os.Getenv("KUBECONFIG") 319 | } 320 | if kubeConfigPath == "" { 321 | kubeConfigPath = clientcmd.RecommendedHomeFile // use default path(.kube/config) 322 | } 323 | return clientcmd.BuildConfigFromFlags("", kubeConfigPath) 324 | } 325 | 326 | // TruncMsg truncates a message to 71 characters 327 | func TruncMsg(msg string) string { 328 | if len(msg) > 71 { 329 | return msg[:71] + "..." 330 | } 331 | return msg 332 | } 333 | --------------------------------------------------------------------------------