mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai
Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).
CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.
Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).
CGO_ENABLED=0 go build ./... verified green.
* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)
Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.
No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.
* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)
Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).
Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.
Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).
* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy
Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.
Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).
CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.
Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.
---------
Co-authored-by: Hanzo Dev <[email protected]>
597 lines
22 KiB
Go
597 lines
22 KiB
Go
package commands
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/mattn/go-isatty"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
|
|
"github.com/hanzoai/deploy/cmd/argocd/commands/headless"
|
|
cmdutil "github.com/hanzoai/deploy/cmd/util"
|
|
"github.com/hanzoai/deploy/common"
|
|
argocdclient "github.com/hanzoai/deploy/pkg/apiclient"
|
|
clusterpkg "github.com/hanzoai/deploy/pkg/apiclient/cluster"
|
|
argoappv1 "github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
|
|
"github.com/hanzoai/deploy/util/cli"
|
|
"github.com/hanzoai/deploy/util/clusterauth"
|
|
"github.com/hanzoai/deploy/util/errors"
|
|
utilio "github.com/hanzoai/deploy/util/io"
|
|
"github.com/hanzoai/deploy/util/text/label"
|
|
)
|
|
|
|
const (
|
|
// type of the cluster ID is 'name'
|
|
clusterIdTypeName = "name"
|
|
// cluster field is 'name'
|
|
clusterFieldName = "name"
|
|
// cluster field is 'namespaces'
|
|
clusterFieldNamespaces = "namespaces"
|
|
// cluster field is 'labels'
|
|
clusterFieldLabel = "labels"
|
|
// cluster field is 'annotations'
|
|
clusterFieldAnnotation = "annotations"
|
|
// indicates managing all namespaces
|
|
allNamespaces = "*"
|
|
)
|
|
|
|
// NewClusterCommand returns a new instance of an `argocd cluster` command
|
|
func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
|
|
command := &cobra.Command{
|
|
Use: "cluster",
|
|
Short: "Manage cluster credentials",
|
|
Run: func(c *cobra.Command, args []string) {
|
|
c.HelpFunc()(c, args)
|
|
os.Exit(1)
|
|
},
|
|
Example: ` # List all known clusters in JSON format:
|
|
argocd cluster list -o json
|
|
|
|
# Add a target cluster configuration to ArgoCD. The context must exist in your kubectl config:
|
|
argocd cluster add example-cluster
|
|
|
|
# Get specific details about a cluster in plain text (wide) format:
|
|
argocd cluster get example-cluster -o wide
|
|
|
|
# Remove a target cluster context from ArgoCD
|
|
argocd cluster rm example-cluster
|
|
|
|
# Set a target cluster context from ArgoCD
|
|
argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace '*'
|
|
argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace namespace-one --namespace namespace-two`,
|
|
}
|
|
|
|
command.AddCommand(NewClusterAddCommand(clientOpts, pathOpts))
|
|
command.AddCommand(NewClusterGetCommand(clientOpts))
|
|
command.AddCommand(NewClusterListCommand(clientOpts))
|
|
command.AddCommand(NewClusterRemoveCommand(clientOpts, pathOpts))
|
|
command.AddCommand(NewClusterRotateAuthCommand(clientOpts))
|
|
command.AddCommand(NewClusterSetCommand(clientOpts))
|
|
return command
|
|
}
|
|
|
|
// NewClusterAddCommand returns a new instance of an `argocd cluster add` command
|
|
func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
|
|
var (
|
|
clusterOpts cmdutil.ClusterOptions
|
|
skipConfirmation bool
|
|
labels []string
|
|
annotations []string
|
|
)
|
|
command := &cobra.Command{
|
|
Use: "add CONTEXT",
|
|
Short: common.CommandCLI + " cluster add CONTEXT",
|
|
Run: func(c *cobra.Command, args []string) {
|
|
ctx := c.Context()
|
|
|
|
var configAccess clientcmd.ConfigAccess = pathOpts
|
|
if len(args) == 0 {
|
|
log.Error("Choose a context name from:")
|
|
cmdutil.PrintKubeContexts(configAccess)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if clusterOpts.InCluster && clusterOpts.ClusterEndpoint != "" {
|
|
log.Fatal("Can only use one of --in-cluster or --cluster-endpoint")
|
|
return
|
|
}
|
|
|
|
contextName := args[0]
|
|
conf, err := getRestConfig(pathOpts, contextName)
|
|
errors.CheckError(err)
|
|
if clusterOpts.ProxyUrl != "" {
|
|
u, err := argoappv1.ParseProxyUrl(clusterOpts.ProxyUrl)
|
|
errors.CheckError(err)
|
|
conf.Proxy = http.ProxyURL(u)
|
|
}
|
|
clientset, err := kubernetes.NewForConfig(conf)
|
|
errors.CheckError(err)
|
|
managerBearerToken := ""
|
|
var awsAuthConf *argoappv1.AWSAuthConfig
|
|
var execProviderConf *argoappv1.ExecProviderConfig
|
|
switch {
|
|
case clusterOpts.AwsClusterName != "":
|
|
awsAuthConf = &argoappv1.AWSAuthConfig{
|
|
ClusterName: clusterOpts.AwsClusterName,
|
|
RoleARN: clusterOpts.AwsRoleArn,
|
|
Profile: clusterOpts.AwsProfile,
|
|
}
|
|
case clusterOpts.ExecProviderCommand != "":
|
|
execProviderConf = &argoappv1.ExecProviderConfig{
|
|
Command: clusterOpts.ExecProviderCommand,
|
|
Args: clusterOpts.ExecProviderArgs,
|
|
Env: clusterOpts.ExecProviderEnv,
|
|
APIVersion: clusterOpts.ExecProviderAPIVersion,
|
|
InstallHint: clusterOpts.ExecProviderInstallHint,
|
|
}
|
|
default:
|
|
// Install RBAC resources for managing the cluster
|
|
if clusterOpts.ServiceAccount != "" {
|
|
managerBearerToken, err = clusterauth.GetServiceAccountBearerToken(clientset, clusterOpts.SystemNamespace, clusterOpts.ServiceAccount, common.BearerTokenTimeout)
|
|
} else {
|
|
isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
|
if isTerminal && !skipConfirmation {
|
|
accessLevel := "cluster"
|
|
if len(clusterOpts.Namespaces) > 0 {
|
|
accessLevel = "namespace"
|
|
}
|
|
message := fmt.Sprintf("WARNING: This will create a service account `argocd-manager` on the cluster referenced by context `%s` with full %s level privileges. Do you want to continue [y/N]? ", contextName, accessLevel)
|
|
if !cli.AskToProceed(message) {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, clusterOpts.SystemNamespace, clusterOpts.Namespaces, common.BearerTokenTimeout)
|
|
}
|
|
errors.CheckError(err)
|
|
}
|
|
|
|
labelsMap, err := label.Parse(labels)
|
|
errors.CheckError(err)
|
|
annotationsMap, err := label.Parse(annotations)
|
|
errors.CheckError(err)
|
|
|
|
conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
|
|
defer utilio.Close(conn)
|
|
if clusterOpts.Name != "" {
|
|
contextName = clusterOpts.Name
|
|
}
|
|
clst := cmdutil.NewCluster(contextName, clusterOpts.Namespaces, clusterOpts.ClusterResources, conf, managerBearerToken, awsAuthConf, execProviderConf, labelsMap, annotationsMap)
|
|
// If --server-proxy-url was explicitly provided, override the proxy that the ArgoCD server will use to
|
|
// reach this cluster. An explicit empty string means "no proxy", which is the common case when the local
|
|
// machine needs a proxy but the two clusters can reach each other directly.
|
|
err = applyServerProxyOverride(c.Flags().Changed("server-proxy-url"), clusterOpts.ServerProxyURL, clst)
|
|
errors.CheckError(err)
|
|
if clusterOpts.InClusterEndpoint() {
|
|
clst.Server = argoappv1.KubernetesInternalAPIServerAddr
|
|
} else if clusterOpts.ClusterEndpoint == string(cmdutil.KubePublicEndpoint) {
|
|
endpoint, caData, err := cmdutil.GetKubePublicEndpoint(clientset)
|
|
if err != nil || endpoint == "" {
|
|
log.Warnf("Failed to find the cluster endpoint from kube-public data: %v", err)
|
|
log.Infof("Falling back to the endpoint '%s' as listed in the kubeconfig context", clst.Server)
|
|
endpoint = clst.Server
|
|
}
|
|
clst.Server = endpoint
|
|
clst.Config.CAData = caData
|
|
}
|
|
|
|
if clusterOpts.Shard >= 0 {
|
|
clst.Shard = &clusterOpts.Shard
|
|
}
|
|
if clusterOpts.Project != "" {
|
|
clst.Project = clusterOpts.Project
|
|
}
|
|
clstCreateReq := clusterpkg.ClusterCreateRequest{
|
|
Cluster: clst,
|
|
Upsert: clusterOpts.Upsert,
|
|
}
|
|
_, err = clusterIf.Create(ctx, &clstCreateReq)
|
|
errors.CheckError(err)
|
|
fmt.Printf("Cluster '%s' added\n", clst.Server)
|
|
},
|
|
}
|
|
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
|
command.Flags().BoolVar(&clusterOpts.Upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
|
|
command.Flags().StringVar(&clusterOpts.ServiceAccount, "service-account", "", fmt.Sprintf("System namespace service account to use for kubernetes resource management. If not set then default %q SA will be created", clusterauth.ArgoCDManagerServiceAccount))
|
|
command.Flags().StringVar(&clusterOpts.SystemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
|
|
command.Flags().BoolVarP(&skipConfirmation, "yes", "y", false, "Skip explicit confirmation")
|
|
command.Flags().StringArrayVar(&labels, "label", nil, "Set metadata labels (e.g. --label key=value)")
|
|
command.Flags().StringArrayVar(&annotations, "annotation", nil, "Set metadata annotations (e.g. --annotation key=value)")
|
|
command.Flags().StringVar(&clusterOpts.ProxyUrl, "proxy-url", "", "use proxy to connect cluster")
|
|
command.Flags().StringVar(&clusterOpts.ServerProxyURL, "server-proxy-url", "", "use a different proxy URL (or \"\" for no proxy) for the ArgoCD server to connect to the cluster; if omitted the value from --proxy-url or the kubeconfig is used")
|
|
cmdutil.AddClusterFlags(command, &clusterOpts)
|
|
return command
|
|
}
|
|
|
|
func getRestConfig(pathOpts *clientcmd.PathOptions, ctxName string) (*rest.Config, error) {
|
|
config, err := pathOpts.GetStartingConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clstContext := config.Contexts[ctxName]
|
|
if clstContext == nil {
|
|
return nil, fmt.Errorf("context %s does not exist in kubeconfig", ctxName)
|
|
}
|
|
|
|
overrides := clientcmd.ConfigOverrides{
|
|
Context: *clstContext,
|
|
}
|
|
|
|
clientConfig := clientcmd.NewDefaultClientConfig(*config, &overrides)
|
|
conf, err := clientConfig.ClientConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conf, nil
|
|
}
|
|
|
|
// applyServerProxyOverride encapsulates the logic for handling the --server-proxy-url flag.
|
|
// When changed is true, it validates non-empty values using argoappv1.ParseProxyUrl and
|
|
// applies the override (including clearing the value when serverProxyURL is an explicit empty string).
|
|
func applyServerProxyOverride(changed bool, serverProxyURL string, clst *argoappv1.Cluster) error {
|
|
if !changed {
|
|
return nil
|
|
}
|
|
if serverProxyURL != "" {
|
|
if _, err := argoappv1.ParseProxyUrl(serverProxyURL); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
clst.Config.ProxyUrl = serverProxyURL
|
|
return nil
|
|
}
|
|
|
|
// NewClusterSetCommand returns a new instance of an `argocd cluster set` command
|
|
func NewClusterSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
|
var (
|
|
clusterOptions cmdutil.ClusterOptions
|
|
clusterName string
|
|
labels []string
|
|
annotations []string
|
|
)
|
|
command := &cobra.Command{
|
|
Use: "set NAME",
|
|
Short: "Set cluster information",
|
|
Example: ` # Set cluster information
|
|
argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace '*'
|
|
argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace namespace-one --namespace namespace-two`,
|
|
Run: func(c *cobra.Command, args []string) {
|
|
ctx := c.Context()
|
|
if len(args) != 1 {
|
|
c.HelpFunc()(c, args)
|
|
os.Exit(1)
|
|
}
|
|
// name of the cluster whose fields have to be updated.
|
|
clusterName = args[0]
|
|
conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
|
|
defer utilio.Close(conn)
|
|
// checks the fields that needs to be updated
|
|
updatedFields := checkFieldsToUpdate(clusterOptions, labels, annotations)
|
|
namespaces := clusterOptions.Namespaces
|
|
// check if all namespaces have to be considered
|
|
if len(namespaces) == 1 && strings.EqualFold(namespaces[0], allNamespaces) {
|
|
namespaces[0] = ""
|
|
}
|
|
// parse the labels you're receiving from the label flag
|
|
labelsMap, err := label.Parse(labels)
|
|
errors.CheckError(err)
|
|
// parse the annotations you're receiving from the annotation flag
|
|
annotationsMap, err := label.Parse(annotations)
|
|
errors.CheckError(err)
|
|
if updatedFields != nil {
|
|
clusterUpdateRequest := clusterpkg.ClusterUpdateRequest{
|
|
Cluster: &argoappv1.Cluster{
|
|
Name: clusterOptions.Name,
|
|
Namespaces: namespaces,
|
|
Labels: labelsMap,
|
|
Annotations: annotationsMap,
|
|
},
|
|
UpdatedFields: updatedFields,
|
|
Id: &clusterpkg.ClusterID{
|
|
Type: clusterIdTypeName,
|
|
Value: clusterName,
|
|
},
|
|
}
|
|
_, err := clusterIf.Update(ctx, &clusterUpdateRequest)
|
|
if err != nil {
|
|
if status.Code(err) == codes.PermissionDenied {
|
|
log.Error("Ensure that the cluster is present and you have the necessary permissions to update the cluster")
|
|
}
|
|
errors.CheckError(err)
|
|
}
|
|
fmt.Printf("Cluster '%s' updated.\n", clusterName)
|
|
} else {
|
|
fmt.Print("Specify the cluster field to be updated.\n")
|
|
}
|
|
},
|
|
}
|
|
command.Flags().StringVar(&clusterOptions.Name, "name", "", "Overwrite the cluster name")
|
|
command.Flags().StringArrayVar(&clusterOptions.Namespaces, "namespace", nil, "List of namespaces which are allowed to manage. Specify '*' to manage all namespaces")
|
|
command.Flags().StringArrayVar(&labels, "label", nil, "Set metadata labels (e.g. --label key=value)")
|
|
command.Flags().StringArrayVar(&annotations, "annotation", nil, "Set metadata annotations (e.g. --annotation key=value)")
|
|
return command
|
|
}
|
|
|
|
// checkFieldsToUpdate returns the fields that needs to be updated
|
|
func checkFieldsToUpdate(clusterOptions cmdutil.ClusterOptions, labels []string, annotations []string) []string {
|
|
var updatedFields []string
|
|
if clusterOptions.Name != "" {
|
|
updatedFields = append(updatedFields, clusterFieldName)
|
|
}
|
|
if clusterOptions.Namespaces != nil {
|
|
updatedFields = append(updatedFields, clusterFieldNamespaces)
|
|
}
|
|
if labels != nil {
|
|
updatedFields = append(updatedFields, clusterFieldLabel)
|
|
}
|
|
if annotations != nil {
|
|
updatedFields = append(updatedFields, clusterFieldAnnotation)
|
|
}
|
|
return updatedFields
|
|
}
|
|
|
|
// NewClusterGetCommand returns a new instance of an `argocd cluster get` command
|
|
func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
|
var output string
|
|
command := &cobra.Command{
|
|
Use: "get SERVER/NAME",
|
|
Short: "Get cluster information",
|
|
Example: `argocd cluster get https://12.34.567.89
|
|
argocd cluster get in-cluster`,
|
|
Run: func(c *cobra.Command, args []string) {
|
|
ctx := c.Context()
|
|
|
|
if len(args) == 0 {
|
|
c.HelpFunc()(c, args)
|
|
os.Exit(1)
|
|
}
|
|
conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
|
|
defer utilio.Close(conn)
|
|
clusters := make([]argoappv1.Cluster, 0)
|
|
for _, clusterSelector := range args {
|
|
clst, err := clusterIf.Get(ctx, getQueryBySelector(clusterSelector))
|
|
errors.CheckError(err)
|
|
clusters = append(clusters, *clst)
|
|
}
|
|
switch output {
|
|
case "yaml", "json":
|
|
err := PrintResourceList(clusters, output, true)
|
|
errors.CheckError(err)
|
|
case "wide", "":
|
|
printClusterDetails(clusters)
|
|
case "server":
|
|
printClusterServers(clusters)
|
|
default:
|
|
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
|
}
|
|
},
|
|
}
|
|
// we have yaml as default to not break backwards-compatibility
|
|
command.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: json|yaml|wide|server")
|
|
return command
|
|
}
|
|
|
|
func strWithDefault(value string, def string) string {
|
|
if value == "" {
|
|
return def
|
|
}
|
|
return value
|
|
}
|
|
|
|
func formatNamespaces(cluster argoappv1.Cluster) string {
|
|
if len(cluster.Namespaces) == 0 {
|
|
return "all namespaces"
|
|
}
|
|
return strings.Join(cluster.Namespaces, ", ")
|
|
}
|
|
|
|
func printClusterDetails(clusters []argoappv1.Cluster) {
|
|
for _, cluster := range clusters {
|
|
fmt.Print("Cluster information\n\n")
|
|
fmt.Printf(" Server URL: %s\n", cluster.Server)
|
|
fmt.Printf(" Server Name: %s\n", strWithDefault(cluster.Name, "-"))
|
|
fmt.Printf(" Server Version: %s\n", cluster.Info.ServerVersion)
|
|
fmt.Printf(" Namespaces: %s\n", formatNamespaces(cluster))
|
|
fmt.Print("\nTLS configuration\n\n")
|
|
fmt.Printf(" Client cert: %v\n", len(cluster.Config.CertData) != 0)
|
|
fmt.Printf(" Cert validation: %v\n", !cluster.Config.Insecure)
|
|
fmt.Print("\nAuthentication\n\n")
|
|
fmt.Printf(" Basic authentication: %v\n", cluster.Config.Username != "")
|
|
fmt.Printf(" oAuth authentication: %v\n", cluster.Config.BearerToken != "")
|
|
fmt.Printf(" AWS authentication: %v\n", cluster.Config.AWSAuthConfig != nil)
|
|
fmt.Printf("\nDisable compression: %v\n", cluster.Config.DisableCompression)
|
|
fmt.Printf("\nUse proxy: %v\n", cluster.Config.ProxyUrl != "")
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// NewClusterRemoveCommand returns a new instance of an `argocd cluster rm` command
|
|
func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
|
|
var noPrompt bool
|
|
command := &cobra.Command{
|
|
Use: "rm SERVER/NAME",
|
|
Short: "Remove cluster credentials",
|
|
Example: `argocd cluster rm https://12.34.567.89
|
|
argocd cluster rm cluster-name`,
|
|
Run: func(c *cobra.Command, args []string) {
|
|
ctx := c.Context()
|
|
|
|
if len(args) == 0 {
|
|
c.HelpFunc()(c, args)
|
|
os.Exit(1)
|
|
}
|
|
conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
|
|
defer utilio.Close(conn)
|
|
numOfClusters := len(args)
|
|
var isConfirmAll bool
|
|
|
|
for _, clusterSelector := range args {
|
|
clusterQuery := getQueryBySelector(clusterSelector)
|
|
var lowercaseAnswer string
|
|
if !noPrompt {
|
|
if numOfClusters == 1 {
|
|
lowercaseAnswer = cli.AskToProceedS("Are you sure you want to remove '" + clusterSelector + "'? Any Apps deploying to this cluster will go to health status Unknown.[y/n] ")
|
|
} else {
|
|
if !isConfirmAll {
|
|
lowercaseAnswer = cli.AskToProceedS("Are you sure you want to remove '" + clusterSelector + "'? Any Apps deploying to this cluster will go to health status Unknown.[y/n/A] where 'A' is to remove all specified clusters without prompting. Any Apps deploying to these clusters will go to health status Unknown. ")
|
|
if lowercaseAnswer == "a" {
|
|
lowercaseAnswer = "y"
|
|
isConfirmAll = true
|
|
}
|
|
} else {
|
|
lowercaseAnswer = "y"
|
|
}
|
|
}
|
|
} else {
|
|
lowercaseAnswer = "y"
|
|
}
|
|
|
|
if lowercaseAnswer == "y" {
|
|
// get the cluster name to use as context to delete RBAC on cluster
|
|
clst, err := clusterIf.Get(ctx, clusterQuery)
|
|
errors.CheckError(err)
|
|
|
|
// remove cluster
|
|
_, err = clusterIf.Delete(ctx, clusterQuery)
|
|
errors.CheckError(err)
|
|
fmt.Printf("Cluster '%s' removed\n", clusterSelector)
|
|
|
|
// remove RBAC from cluster
|
|
conf, err := getRestConfig(pathOpts, clst.Name)
|
|
errors.CheckError(err)
|
|
|
|
clientset, err := kubernetes.NewForConfig(conf)
|
|
errors.CheckError(err)
|
|
|
|
err = clusterauth.UninstallClusterManagerRBAC(clientset)
|
|
errors.CheckError(err)
|
|
} else {
|
|
fmt.Println("The command to remove '" + clusterSelector + "' was cancelled.")
|
|
}
|
|
}
|
|
},
|
|
}
|
|
command.Flags().BoolVarP(&noPrompt, "yes", "y", false, "Turn off prompting to confirm remove of cluster resources")
|
|
return command
|
|
}
|
|
|
|
// Print table of cluster information
|
|
func printClusterTable(clusters []argoappv1.Cluster) {
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
_, _ = fmt.Fprint(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\tPROJECT\n")
|
|
for _, c := range clusters {
|
|
server := c.Server
|
|
if len(c.Namespaces) > 0 {
|
|
server = fmt.Sprintf("%s (%d namespaces)", c.Server, len(c.Namespaces))
|
|
}
|
|
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", server, c.Name, c.Info.ServerVersion, c.Info.ConnectionState.Status, c.Info.ConnectionState.Message, c.Project)
|
|
}
|
|
_ = w.Flush()
|
|
}
|
|
|
|
// Returns cluster query for getting cluster depending on the cluster selector
|
|
func getQueryBySelector(clusterSelector string) *clusterpkg.ClusterQuery {
|
|
var query clusterpkg.ClusterQuery
|
|
isServer, err := regexp.MatchString(`^https?://`, clusterSelector)
|
|
if isServer || err != nil {
|
|
query.Server = clusterSelector
|
|
} else {
|
|
query.Name = clusterSelector
|
|
}
|
|
return &query
|
|
}
|
|
|
|
// Print list of cluster servers
|
|
func printClusterServers(clusters []argoappv1.Cluster) {
|
|
for _, c := range clusters {
|
|
fmt.Println(c.Server)
|
|
}
|
|
}
|
|
|
|
// NewClusterListCommand returns a new instance of an `argocd cluster rm` command
|
|
func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
|
var output string
|
|
command := &cobra.Command{
|
|
Use: "list",
|
|
Short: "List configured clusters",
|
|
Run: func(c *cobra.Command, _ []string) {
|
|
ctx := c.Context()
|
|
|
|
conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
|
|
defer utilio.Close(conn)
|
|
clusters, err := clusterIf.List(ctx, &clusterpkg.ClusterQuery{})
|
|
errors.CheckError(err)
|
|
switch output {
|
|
case "yaml", "json":
|
|
err := PrintResourceList(clusters.Items, output, false)
|
|
errors.CheckError(err)
|
|
case "server":
|
|
printClusterServers(clusters.Items)
|
|
case "wide", "":
|
|
printClusterTable(clusters.Items)
|
|
default:
|
|
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
|
}
|
|
},
|
|
Example: `
|
|
# List Clusters in Default "Wide" Format
|
|
argocd cluster list
|
|
|
|
# List Cluster via specifying the server
|
|
argocd cluster list --server <ARGOCD_SERVER_ADDRESS>
|
|
|
|
# List Clusters in JSON Format
|
|
argocd cluster list -o json --server <ARGOCD_SERVER_ADDRESS>
|
|
|
|
# List Clusters in YAML Format
|
|
argocd cluster list -o yaml --server <ARGOCD_SERVER_ADDRESS>
|
|
|
|
# List Clusters that have been added to your Argo CD
|
|
argocd cluster list -o server <ARGOCD_SERVER_ADDRESS>
|
|
|
|
`,
|
|
}
|
|
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|server")
|
|
return command
|
|
}
|
|
|
|
// NewClusterRotateAuthCommand returns a new instance of an `argocd cluster rotate-auth` command
|
|
func NewClusterRotateAuthCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
|
command := &cobra.Command{
|
|
Use: "rotate-auth SERVER/NAME",
|
|
Short: common.CommandCLI + " cluster rotate-auth SERVER/NAME",
|
|
Example: `argocd cluster rotate-auth https://12.34.567.89
|
|
argocd cluster rotate-auth cluster-name`,
|
|
Run: func(c *cobra.Command, args []string) {
|
|
ctx := c.Context()
|
|
|
|
if len(args) != 1 {
|
|
c.HelpFunc()(c, args)
|
|
os.Exit(1)
|
|
}
|
|
conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
|
|
defer utilio.Close(conn)
|
|
|
|
cluster := args[0]
|
|
clusterQuery := getQueryBySelector(cluster)
|
|
_, err := clusterIf.RotateAuth(ctx, clusterQuery)
|
|
errors.CheckError(err)
|
|
|
|
fmt.Printf("Cluster '%s' rotated auth\n", cluster)
|
|
},
|
|
}
|
|
return command
|
|
}
|