|
| 1 | +/* |
| 2 | +Copyright 2020 The Flux authors |
| 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 main |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "fmt" |
| 22 | + "os" |
| 23 | + "time" |
| 24 | + |
| 25 | + "github.com/fluxcd/pkg/git" |
| 26 | + "github.com/fluxcd/pkg/git/gogit" |
| 27 | + "github.com/spf13/cobra" |
| 28 | + |
| 29 | + "github.com/fluxcd/flux2/v2/internal/flags" |
| 30 | + "github.com/fluxcd/flux2/v2/internal/utils" |
| 31 | + "github.com/fluxcd/flux2/v2/pkg/bootstrap" |
| 32 | + "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" |
| 33 | + "github.com/fluxcd/flux2/v2/pkg/manifestgen" |
| 34 | + "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" |
| 35 | + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" |
| 36 | + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" |
| 37 | +) |
| 38 | + |
| 39 | +var bootstrapGiteaCmd = &cobra.Command{ |
| 40 | + Use: "gitea", |
| 41 | + Short: "Deploy Flux on a cluster connected to a Gitea repository", |
| 42 | + Long: `The bootstrap gitea command creates the Gitea repository if it doesn't exists and |
| 43 | +commits the Flux manifests to the specified branch. |
| 44 | +Then it configures the target cluster to synchronize with that repository. |
| 45 | +If the Flux components are present on the cluster, |
| 46 | +the bootstrap command will perform an upgrade if needed.`, |
| 47 | + Example: ` # Create a Gitea personal access token and export it as an env var |
| 48 | + export GITEA_TOKEN=<my-token> |
| 49 | +
|
| 50 | + # Run bootstrap for a private repository owned by a Gitea organization |
| 51 | + flux bootstrap gitea --owner=<organization> --repository=<repository name> --path=clusters/my-cluster |
| 52 | +
|
| 53 | + # Run bootstrap for a private repository and assign organization teams to it |
| 54 | + flux bootstrap gitea --owner=<organization> --repository=<repository name> --team=<team1 slug> --team=<team2 slug> --path=clusters/my-cluster |
| 55 | +
|
| 56 | + # Run bootstrap for a private repository and assign organization teams with their access level(e.g maintain, admin) to it |
| 57 | + flux bootstrap gitea --owner=<organization> --repository=<repository name> --team=<team1 slug>:<access-level> --path=clusters/my-cluster |
| 58 | +
|
| 59 | + # Run bootstrap for a public repository on a personal account |
| 60 | + flux bootstrap gitea --owner=<user> --repository=<repository name> --private=false --personal=true --path=clusters/my-cluster |
| 61 | +
|
| 62 | + # Run bootstrap for a private repository hosted on Gitea Enterprise using SSH auth |
| 63 | + flux bootstrap gitea --owner=<organization> --repository=<repository name> --hostname=<domain> --ssh-hostname=<domain> --path=clusters/my-cluster |
| 64 | +
|
| 65 | + # Run bootstrap for a private repository hosted on Gitea Enterprise using HTTPS auth |
| 66 | + flux bootstrap gitea --owner=<organization> --repository=<repository name> --hostname=<domain> --token-auth --path=clusters/my-cluster |
| 67 | +
|
| 68 | + # Run bootstrap for an existing repository with a branch named main |
| 69 | + flux bootstrap gitea --owner=<organization> --repository=<repository name> --branch=main --path=clusters/my-cluster`, |
| 70 | + RunE: bootstrapGiteaCmdRun, |
| 71 | +} |
| 72 | + |
| 73 | +type giteaFlags struct { |
| 74 | + owner string |
| 75 | + repository string |
| 76 | + interval time.Duration |
| 77 | + personal bool |
| 78 | + private bool |
| 79 | + hostname string |
| 80 | + path flags.SafeRelativePath |
| 81 | + teams []string |
| 82 | + readWriteKey bool |
| 83 | + reconcile bool |
| 84 | +} |
| 85 | + |
| 86 | +const ( |
| 87 | + gtDefaultPermission = "maintain" |
| 88 | + gtDefaultDomain = "gitea.com" |
| 89 | + gtTokenEnvVar = "GITEA_TOKEN" |
| 90 | +) |
| 91 | + |
| 92 | +var giteaArgs giteaFlags |
| 93 | + |
| 94 | +func init() { |
| 95 | + bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.owner, "owner", "", "Gitea user or organization name") |
| 96 | + bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.repository, "repository", "", "Gitea repository name") |
| 97 | + bootstrapGiteaCmd.Flags().StringSliceVar(&giteaArgs.teams, "team", []string{}, "Gitea team and the access to be given to it(team:maintain). Defaults to maintainer access if no access level is specified (also accepts comma-separated values)") |
| 98 | + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.personal, "personal", false, "if true, the owner is assumed to be a Gitea user; otherwise an org") |
| 99 | + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.private, "private", true, "if true, the repository is setup or configured as private") |
| 100 | + bootstrapGiteaCmd.Flags().DurationVar(&giteaArgs.interval, "interval", time.Minute, "sync interval") |
| 101 | + bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.hostname, "hostname", gtDefaultDomain, "Gitea hostname") |
| 102 | + bootstrapGiteaCmd.Flags().Var(&giteaArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") |
| 103 | + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") |
| 104 | + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") |
| 105 | + |
| 106 | + bootstrapCmd.AddCommand(bootstrapGiteaCmd) |
| 107 | +} |
| 108 | + |
| 109 | +func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error { |
| 110 | + gtToken := os.Getenv(gtTokenEnvVar) |
| 111 | + if gtToken == "" { |
| 112 | + var err error |
| 113 | + gtToken, err = readPasswordFromStdin("Please enter your Gitea personal access token (PAT): ") |
| 114 | + if err != nil { |
| 115 | + return fmt.Errorf("could not read token: %w", err) |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + if err := bootstrapValidate(); err != nil { |
| 120 | + return err |
| 121 | + } |
| 122 | + |
| 123 | + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) |
| 124 | + defer cancel() |
| 125 | + |
| 126 | + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) |
| 127 | + if err != nil { |
| 128 | + return err |
| 129 | + } |
| 130 | + |
| 131 | + // Manifest base |
| 132 | + if ver, err := getVersion(bootstrapArgs.version); err != nil { |
| 133 | + return err |
| 134 | + } else { |
| 135 | + bootstrapArgs.version = ver |
| 136 | + } |
| 137 | + manifestsBase, err := buildEmbeddedManifestBase() |
| 138 | + if err != nil { |
| 139 | + return err |
| 140 | + } |
| 141 | + defer os.RemoveAll(manifestsBase) |
| 142 | + |
| 143 | + var caBundle []byte |
| 144 | + if bootstrapArgs.caFile != "" { |
| 145 | + var err error |
| 146 | + caBundle, err = os.ReadFile(bootstrapArgs.caFile) |
| 147 | + if err != nil { |
| 148 | + return fmt.Errorf("unable to read TLS CA file: %w", err) |
| 149 | + } |
| 150 | + } |
| 151 | + // Build Gitea provider |
| 152 | + providerCfg := provider.Config{ |
| 153 | + Provider: provider.GitProviderGitea, |
| 154 | + Hostname: giteaArgs.hostname, |
| 155 | + Token: gtToken, |
| 156 | + CaBundle: caBundle, |
| 157 | + } |
| 158 | + providerClient, err := provider.BuildGitProvider(providerCfg) |
| 159 | + if err != nil { |
| 160 | + return err |
| 161 | + } |
| 162 | + |
| 163 | + tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") |
| 164 | + if err != nil { |
| 165 | + return fmt.Errorf("failed to create temporary working dir: %w", err) |
| 166 | + } |
| 167 | + defer os.RemoveAll(tmpDir) |
| 168 | + |
| 169 | + clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} |
| 170 | + gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ |
| 171 | + Transport: git.HTTPS, |
| 172 | + Username: giteaArgs.owner, |
| 173 | + Password: gtToken, |
| 174 | + CAFile: caBundle, |
| 175 | + }, clientOpts...) |
| 176 | + if err != nil { |
| 177 | + return fmt.Errorf("failed to create a Git client: %w", err) |
| 178 | + } |
| 179 | + |
| 180 | + // Install manifest config |
| 181 | + installOptions := install.Options{ |
| 182 | + BaseURL: rootArgs.defaults.BaseURL, |
| 183 | + Version: bootstrapArgs.version, |
| 184 | + Namespace: *kubeconfigArgs.Namespace, |
| 185 | + Components: bootstrapComponents(), |
| 186 | + Registry: bootstrapArgs.registry, |
| 187 | + ImagePullSecret: bootstrapArgs.imagePullSecret, |
| 188 | + WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, |
| 189 | + NetworkPolicy: bootstrapArgs.networkPolicy, |
| 190 | + LogLevel: bootstrapArgs.logLevel.String(), |
| 191 | + NotificationController: rootArgs.defaults.NotificationController, |
| 192 | + ManifestFile: rootArgs.defaults.ManifestFile, |
| 193 | + Timeout: rootArgs.timeout, |
| 194 | + TargetPath: giteaArgs.path.ToSlash(), |
| 195 | + ClusterDomain: bootstrapArgs.clusterDomain, |
| 196 | + TolerationKeys: bootstrapArgs.tolerationKeys, |
| 197 | + } |
| 198 | + if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { |
| 199 | + installOptions.BaseURL = customBaseURL |
| 200 | + } |
| 201 | + |
| 202 | + // Source generation and secret config |
| 203 | + secretOpts := sourcesecret.Options{ |
| 204 | + Name: bootstrapArgs.secretName, |
| 205 | + Namespace: *kubeconfigArgs.Namespace, |
| 206 | + TargetPath: giteaArgs.path.ToSlash(), |
| 207 | + ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, |
| 208 | + } |
| 209 | + if bootstrapArgs.tokenAuth { |
| 210 | + secretOpts.Username = "git" |
| 211 | + secretOpts.Password = gtToken |
| 212 | + secretOpts.CAFile = caBundle |
| 213 | + } else { |
| 214 | + secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) |
| 215 | + secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) |
| 216 | + secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve |
| 217 | + |
| 218 | + secretOpts.SSHHostname = giteaArgs.hostname |
| 219 | + if bootstrapArgs.sshHostname != "" { |
| 220 | + secretOpts.SSHHostname = bootstrapArgs.sshHostname |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + // Sync manifest config |
| 225 | + syncOpts := sync.Options{ |
| 226 | + Interval: giteaArgs.interval, |
| 227 | + Name: *kubeconfigArgs.Namespace, |
| 228 | + Namespace: *kubeconfigArgs.Namespace, |
| 229 | + Branch: bootstrapArgs.branch, |
| 230 | + Secret: bootstrapArgs.secretName, |
| 231 | + TargetPath: giteaArgs.path.ToSlash(), |
| 232 | + ManifestFile: sync.MakeDefaultOptions().ManifestFile, |
| 233 | + RecurseSubmodules: bootstrapArgs.recurseSubmodules, |
| 234 | + } |
| 235 | + |
| 236 | + entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) |
| 237 | + if err != nil { |
| 238 | + return err |
| 239 | + } |
| 240 | + |
| 241 | + // Bootstrap config |
| 242 | + bootstrapOpts := []bootstrap.GitProviderOption{ |
| 243 | + bootstrap.WithProviderRepository(giteaArgs.owner, giteaArgs.repository, giteaArgs.personal), |
| 244 | + bootstrap.WithBranch(bootstrapArgs.branch), |
| 245 | + bootstrap.WithBootstrapTransportType("https"), |
| 246 | + bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), |
| 247 | + bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), |
| 248 | + bootstrap.WithProviderTeamPermissions(mapTeamSlice(giteaArgs.teams, gtDefaultPermission)), |
| 249 | + bootstrap.WithReadWriteKeyPermissions(giteaArgs.readWriteKey), |
| 250 | + bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), |
| 251 | + bootstrap.WithLogger(logger), |
| 252 | + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), |
| 253 | + } |
| 254 | + if bootstrapArgs.sshHostname != "" { |
| 255 | + bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) |
| 256 | + } |
| 257 | + if bootstrapArgs.tokenAuth { |
| 258 | + bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) |
| 259 | + } |
| 260 | + if !giteaArgs.private { |
| 261 | + bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) |
| 262 | + } |
| 263 | + if giteaArgs.reconcile { |
| 264 | + bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) |
| 265 | + } |
| 266 | + |
| 267 | + // Setup bootstrapper with constructed configs |
| 268 | + b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) |
| 269 | + if err != nil { |
| 270 | + return err |
| 271 | + } |
| 272 | + |
| 273 | + // Run |
| 274 | + return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) |
| 275 | +} |
0 commit comments