From c35899ed6ed0179a9051647edb63bc936ca9e17d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 29 Jan 2026 20:21:31 +0000 Subject: [PATCH] Add CTRLC_ env support via viper Co-authored-by: justin --- README.md | 12 ++++++---- cmd/ctrlc/ctrlc.go | 25 ++++++++++++++------ cmd/ctrlc/root/root.go | 24 +++++++------------ cmd/ctrlc/root/sync/helm/helm.go | 4 ++-- cmd/ctrlc/root/sync/kubernetes/kubernetes.go | 2 +- cmd/ctrlc/root/sync/kubernetes/vcluster.go | 4 ++-- cmd/ctrlc/root/sync/salesforce/README.md | 8 +++---- cmd/ctrlc/root/sync/salesforce/salesforce.go | 20 +++++++--------- docker/README.md | 6 ++--- 9 files changed, 54 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index f47f4be..82452b5 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,14 @@ workspace: your-workspace-id ### Environment Variables ```bash -export CTRLPLANE_URL="https://app.ctrlplane.dev" -export CTRLPLANE_API_KEY="your-api-key-here" -export CTRLPLANE_WORKSPACE="your-workspace-id" -export CTRLPLANE_CLUSTER_IDENTIFIER="my-cluster" +export CTRLC_URL="https://app.ctrlplane.dev" +export CTRLC_API_KEY="your-api-key-here" +export CTRLC_WORKSPACE="your-workspace-id" +export CTRLC_CLUSTER_IDENTIFIER="my-cluster" ``` +Any flag or config key can also be set by prefixing it with `CTRLC_` and replacing `-` or `.` with `_`. + ### Command-Line Flags ```bash @@ -407,7 +409,7 @@ The CLI includes GitHub Actions for CI/CD workflows. See the `actions/` director - uses: ctrlplanedev/cli/actions/get-resource@main with: resource-id: ${{ env.RESOURCE_ID }} - api-key: ${{ secrets.CTRLPLANE_API_KEY }} + api-key: ${{ secrets.CTRLC_API_KEY }} ``` ## Docker diff --git a/cmd/ctrlc/ctrlc.go b/cmd/ctrlc/ctrlc.go index a58e993..85719d9 100644 --- a/cmd/ctrlc/ctrlc.go +++ b/cmd/ctrlc/ctrlc.go @@ -2,6 +2,7 @@ package main import ( "os" + "strings" "github.com/charmbracelet/log" "github.com/ctrlplanedev/cli/cmd/ctrlc/root" @@ -16,23 +17,28 @@ var ( ) func init() { + viper.SetEnvPrefix("CTRLC") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + viper.AutomaticEnv() + cobra.OnInitialize(initConfig) cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Config file (default is $HOME/.ctrlc.yaml)") - viper.BindEnv("config", "CTRLPLANE_CONFIG") + viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")) + viper.BindEnv("config", "CTRLC_CONFIG", "CTRLPLANE_CONFIG") cmd.PersistentFlags().String("url", "https://app.ctrlplane.dev", "API URL") viper.BindPFlag("url", cmd.PersistentFlags().Lookup("url")) - viper.BindEnv("url", "CTRLPLANE_URL") + viper.BindEnv("url", "CTRLC_URL", "CTRLPLANE_URL") cmd.PersistentFlags().String("api-key", "", "API key for authentication") viper.BindPFlag("api-key", cmd.PersistentFlags().Lookup("api-key")) - viper.BindEnv("api-key", "CTRLPLANE_API_KEY") + viper.BindEnv("api-key", "CTRLC_API_KEY", "CTRLPLANE_API_KEY") cmd.PersistentFlags().String("workspace", "", "Ctrlplane Workspace ID") viper.BindPFlag("workspace", cmd.PersistentFlags().Lookup("workspace")) - viper.BindEnv("workspace", "CTRLPLANE_WORKSPACE") + viper.BindEnv("workspace", "CTRLC_WORKSPACE", "CTRLPLANE_WORKSPACE") - viper.BindEnv("cluster-identifier", "CTRLPLANE_CLUSTER_IDENTIFIER") + viper.BindEnv("cluster-identifier", "CTRLC_CLUSTER_IDENTIFIER", "CTRLPLANE_CLUSTER_IDENTIFIER") } func main() { @@ -42,8 +48,13 @@ func main() { } func initConfig() { - if cfgFile != "" { - viper.SetConfigFile(cfgFile) + configFile := cfgFile + if configFile == "" { + configFile = viper.GetString("config") + } + + if configFile != "" { + viper.SetConfigFile(configFile) } else { // Find home directory. home, err := homedir.Dir() diff --git a/cmd/ctrlc/root/root.go b/cmd/ctrlc/root/root.go index 8c5a883..7526c9c 100644 --- a/cmd/ctrlc/root/root.go +++ b/cmd/ctrlc/root/root.go @@ -1,8 +1,6 @@ package root import ( - "os" - "github.com/MakeNowJust/heredoc/v2" "github.com/charmbracelet/log" @@ -14,11 +12,10 @@ import ( "github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync" "github.com/ctrlplanedev/cli/cmd/ctrlc/root/version" "github.com/spf13/cobra" + "github.com/spf13/viper" ) func NewRootCmd() *cobra.Command { - var logLevel string - cmd := &cobra.Command{ Use: "ctrlc [subcommand] [flags]", Short: "Ctrlplane CLI", @@ -28,6 +25,11 @@ func NewRootCmd() *cobra.Command { $ ctrlc connect `), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + logLevel := viper.GetString("log-level") + if logLevel == "" { + logLevel = "info" + } + switch logLevel { case "debug": log.SetLevel(log.DebugLevel) @@ -45,7 +47,8 @@ func NewRootCmd() *cobra.Command { }, } - cmd.PersistentFlags().StringVar(&logLevel, "log-level", defaultOrEnv("info", "CTRLC_LOG_LEVEL"), "Set the logging level (debug, info, warn, error)") + cmd.PersistentFlags().String("log-level", "info", "Set the logging level (debug, info, warn, error)") + viper.BindPFlag("log-level", cmd.PersistentFlags().Lookup("log-level")) cmd.AddCommand(agent.NewAgentCmd()) cmd.AddCommand(api.NewAPICmd()) @@ -58,14 +61,3 @@ func NewRootCmd() *cobra.Command { return cmd } - -func defaultOrEnv(defaultValue string, envVarName string) string { - if envVarName == "" { - return defaultValue - } - value, set := os.LookupEnv(envVarName) - if !set { - value = defaultValue - } - return value -} diff --git a/cmd/ctrlc/root/sync/helm/helm.go b/cmd/ctrlc/root/sync/helm/helm.go index bcadf66..e7ce888 100644 --- a/cmd/ctrlc/root/sync/helm/helm.go +++ b/cmd/ctrlc/root/sync/helm/helm.go @@ -139,7 +139,7 @@ func NewSyncHelmCmd() *cobra.Command { } cmd.Flags().StringVarP(&providerName, "provider", "p", "", "Name of the resource provider") - cmd.Flags().StringVarP(&clusterIdentifier, "cluster-identifier", "c", "", "The identifier of the parent cluster in ctrlplane (if not provided, will use the CLUSTER_IDENTIFIER environment variable)") + cmd.Flags().StringVarP(&clusterIdentifier, "cluster-identifier", "c", "", "The identifier of the parent cluster in ctrlplane (if not provided, will use the CTRLC_CLUSTER_IDENTIFIER environment variable)") cmd.Flags().StringVarP(&clusterName, "cluster-name", "n", "", "The name of the cluster") cmd.Flags().StringVar(&namespace, "namespace", "", "Kubernetes namespace to sync Helm releases from (if not provided, syncs from all namespaces)") @@ -165,7 +165,7 @@ func initializeCtrlplaneClient() (*api.ClientWithResponses, string, error) { // resolveClusterConfig determines the cluster name and identifier from multiple sources: // 1. Explicit --cluster-identifier flag (takes precedence) -// 2. Environment variable CLUSTER_IDENTIFIER +// 2. Environment variable CTRLC_CLUSTER_IDENTIFIER // 3. Kubeconfig current-context (fallback) // // If a cluster identifier is provided, we try to fetch the cluster resource from Ctrlplane diff --git a/cmd/ctrlc/root/sync/kubernetes/kubernetes.go b/cmd/ctrlc/root/sync/kubernetes/kubernetes.go index b86dec4..3270a80 100644 --- a/cmd/ctrlc/root/sync/kubernetes/kubernetes.go +++ b/cmd/ctrlc/root/sync/kubernetes/kubernetes.go @@ -153,7 +153,7 @@ func NewSyncKubernetesCmd() *cobra.Command { }, } cmd.Flags().StringVarP(&providerName, "provider", "p", "", "Name of the resource provider") - cmd.Flags().StringVarP(&clusterIdentifier, "cluster-identifier", "c", "", "The identifier of the parent cluster in ctrlplane (if not provided, will use the CLUSTER_IDENTIFIER environment variable)") + cmd.Flags().StringVarP(&clusterIdentifier, "cluster-identifier", "c", "", "The identifier of the parent cluster in ctrlplane (if not provided, will use the CTRLC_CLUSTER_IDENTIFIER environment variable)") cmd.Flags().StringVarP(&clusterName, "cluster-name", "n", "", "The name of the cluster") return cmd diff --git a/cmd/ctrlc/root/sync/kubernetes/vcluster.go b/cmd/ctrlc/root/sync/kubernetes/vcluster.go index f308a5c..ecbba7e 100644 --- a/cmd/ctrlc/root/sync/kubernetes/vcluster.go +++ b/cmd/ctrlc/root/sync/kubernetes/vcluster.go @@ -198,7 +198,7 @@ func NewSyncVclusterCmd() *cobra.Command { } if clusterIdentifier == "" { - return fmt.Errorf("cluster identifier is required, please set the CTRLPLANE_CLUSTER_IDENTIFIER environment variable or use the --cluster-identifier flag") + return fmt.Errorf("cluster identifier is required, please set the CTRLC_CLUSTER_IDENTIFIER environment variable or use the --cluster-identifier flag") } ctrlplaneClient, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey) @@ -255,7 +255,7 @@ func NewSyncVclusterCmd() *cobra.Command { }, } - cmd.Flags().StringVarP(&clusterIdentifier, "cluster-identifier", "c", "", "The identifier of the parent cluster in ctrlplane (if not provided, will use the CLUSTER_IDENTIFIER environment variable)") + cmd.Flags().StringVarP(&clusterIdentifier, "cluster-identifier", "c", "", "The identifier of the parent cluster in ctrlplane (if not provided, will use the CTRLC_CLUSTER_IDENTIFIER environment variable)") cmd.Flags().StringVarP(&providerName, "provider", "p", "", "The name of the resource provider (optional)") return cmd diff --git a/cmd/ctrlc/root/sync/salesforce/README.md b/cmd/ctrlc/root/sync/salesforce/README.md index 8467e4b..1b146d7 100644 --- a/cmd/ctrlc/root/sync/salesforce/README.md +++ b/cmd/ctrlc/root/sync/salesforce/README.md @@ -6,9 +6,9 @@ Sync Salesforce CRM data (Accounts, Opportunities) into Ctrlplane as resources. ```bash # Set credentials (via environment or flags) -export SALESFORCE_DOMAIN="https://mycompany.my.salesforce.com" -export SALESFORCE_CONSUMER_KEY="your-key" -export SALESFORCE_CONSUMER_SECRET="your-secret" +export CTRLC_SALESFORCE_DOMAIN="https://mycompany.my.salesforce.com" +export CTRLC_SALESFORCE_CONSUMER_KEY="your-key" +export CTRLC_SALESFORCE_CONSUMER_SECRET="your-secret" # Sync all accounts ctrlc sync salesforce accounts @@ -27,7 +27,7 @@ ctrlc sync salesforce accounts \ Requires Salesforce OAuth2 credentials from a Connected App with `api` and `refresh_token` scopes. Credentials can be provided via: -- Environment variables: `SALESFORCE_DOMAIN`, `SALESFORCE_CONSUMER_KEY`, `SALESFORCE_CONSUMER_SECRET` +- Environment variables: `CTRLC_SALESFORCE_DOMAIN`, `CTRLC_SALESFORCE_CONSUMER_KEY`, `CTRLC_SALESFORCE_CONSUMER_SECRET` - Command flags: `--salesforce-domain`, `--salesforce-consumer-key`, `--salesforce-consumer-secret` ## Common Flags diff --git a/cmd/ctrlc/root/sync/salesforce/salesforce.go b/cmd/ctrlc/root/sync/salesforce/salesforce.go index e0e385a..4b970c6 100644 --- a/cmd/ctrlc/root/sync/salesforce/salesforce.go +++ b/cmd/ctrlc/root/sync/salesforce/salesforce.go @@ -29,20 +29,18 @@ func NewSalesforceCmd() *cobra.Command { `), } - cmd.PersistentFlags().String("salesforce-domain", "", "Salesforce domain (e.g., https://my-domain.my.salesforce.com) (can also be set via SALESFORCE_DOMAIN env var)") - cmd.PersistentFlags().String("salesforce-consumer-key", "", "Salesforce consumer key (can also be set via SALESFORCE_CONSUMER_KEY env var)") - cmd.PersistentFlags().String("salesforce-consumer-secret", "", "Salesforce consumer secret (can also be set via SALESFORCE_CONSUMER_SECRET env var)") + cmd.PersistentFlags().String("salesforce-domain", "", "Salesforce domain (e.g., https://my-domain.my.salesforce.com) (can also be set via CTRLC_SALESFORCE_DOMAIN env var)") + cmd.PersistentFlags().String("salesforce-consumer-key", "", "Salesforce consumer key (can also be set via CTRLC_SALESFORCE_CONSUMER_KEY env var)") + cmd.PersistentFlags().String("salesforce-consumer-secret", "", "Salesforce consumer secret (can also be set via CTRLC_SALESFORCE_CONSUMER_SECRET env var)") - viper.AutomaticEnv() - - if err := viper.BindEnv("salesforce-domain", "SALESFORCE_DOMAIN"); err != nil { - panic(fmt.Errorf("failed to bind SALESFORCE_DOMAIN env var: %w", err)) + if err := viper.BindEnv("salesforce-domain", "CTRLC_SALESFORCE_DOMAIN", "SALESFORCE_DOMAIN"); err != nil { + panic(fmt.Errorf("failed to bind CTRLC_SALESFORCE_DOMAIN env var: %w", err)) } - if err := viper.BindEnv("salesforce-consumer-key", "SALESFORCE_CONSUMER_KEY"); err != nil { - panic(fmt.Errorf("failed to bind SALESFORCE_CONSUMER_KEY env var: %w", err)) + if err := viper.BindEnv("salesforce-consumer-key", "CTRLC_SALESFORCE_CONSUMER_KEY", "SALESFORCE_CONSUMER_KEY"); err != nil { + panic(fmt.Errorf("failed to bind CTRLC_SALESFORCE_CONSUMER_KEY env var: %w", err)) } - if err := viper.BindEnv("salesforce-consumer-secret", "SALESFORCE_CONSUMER_SECRET"); err != nil { - panic(fmt.Errorf("failed to bind SALESFORCE_CONSUMER_SECRET env var: %w", err)) + if err := viper.BindEnv("salesforce-consumer-secret", "CTRLC_SALESFORCE_CONSUMER_SECRET", "SALESFORCE_CONSUMER_SECRET"); err != nil { + panic(fmt.Errorf("failed to bind CTRLC_SALESFORCE_CONSUMER_SECRET env var: %w", err)) } if err := viper.BindPFlag("salesforce-domain", cmd.PersistentFlags().Lookup("salesforce-domain")); err != nil { diff --git a/docker/README.md b/docker/README.md index 59a3772..efc85fe 100644 --- a/docker/README.md +++ b/docker/README.md @@ -24,8 +24,8 @@ docker run ctrlplane/cli ctrlc [your-command] ### Required environment variables -- `CTRLPLANE_API_KEY`: Your Ctrlplane API key. -- `CTRLPLANE_URL`: The URL of your Ctrlplane instance (e.g. `https://app.ctrlplane.dev`). +- `CTRLC_API_KEY`: Your Ctrlplane API key. +- `CTRLC_URL`: The URL of your Ctrlplane instance (e.g. `https://app.ctrlplane.dev`). ### Terraform sync @@ -35,6 +35,6 @@ In order to sync Terraform resources into Ctrlplane, you need to set the followi - `TFE_ADDRESS` (optional): The URL of your Terraform Cloud instance (e.g. `https://app.terraform.io`). If not set, the default address (`https://app.terraform.io`) is used. ```sh -docker run -e TFE_TOKEN=my-token -e CTRLPLANE_API_KEY=my-api-key -e CTRLPLANE_URL=https://app.ctrlplane.dev \ +docker run -e TFE_TOKEN=my-token -e CTRLC_API_KEY=my-api-key -e CTRLC_URL=https://app.ctrlplane.dev \ ctrlplane/cli ctrlc sync terraform --organization my-org --workspace 2a7c5560-75c9-4dbe-be74-04ee33bf8188 ```