m/c/metroctl: add configurable credentials path

This lets metroctl users provide an alternative path to their cluster
credentials. This will be used by the upcoming metroctl test harness.

Change-Id: I49647e3b9d038c230b9678ebb73ba19da038a6d7
Reviewed-on: https://review.monogon.dev/c/monogon/+/833
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/cli/metroctl/credentials.go b/metropolis/cli/metroctl/credentials.go
index 5c23f50..a8885bb 100644
--- a/metropolis/cli/metroctl/credentials.go
+++ b/metropolis/cli/metroctl/credentials.go
@@ -8,8 +8,6 @@
 	"fmt"
 	"os"
 	"path/filepath"
-
-	"github.com/adrg/xdg"
 )
 
 var noCredentialsError = errors.New("owner certificate or key does not exist")
@@ -17,7 +15,7 @@
 // getCredentials returns Metropolis credentials (if any) from the current
 // metroctl config directory.
 func getCredentials() (cert *x509.Certificate, key ed25519.PrivateKey, err error) {
-	ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
+	ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem"))
 	if os.IsNotExist(err) {
 		return nil, nil, noCredentialsError
 	} else if err != nil {
@@ -34,7 +32,7 @@
 		return nil, nil, errors.New("owner-key.pem contains a non-Ed25519 key")
 	}
 	key = block.Bytes
-	ownerCertPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner.pem"))
+	ownerCertPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner.pem"))
 	if os.IsNotExist(err) {
 		return nil, nil, noCredentialsError
 	} else if err != nil {
diff --git a/metropolis/cli/metroctl/install.go b/metropolis/cli/metroctl/install.go
index cbbb9b6..38b82bf 100644
--- a/metropolis/cli/metroctl/install.go
+++ b/metropolis/cli/metroctl/install.go
@@ -12,7 +12,6 @@
 	"os"
 	"path/filepath"
 
-	"github.com/adrg/xdg"
 	"github.com/spf13/cobra"
 
 	"source.monogon.dev/metropolis/cli/metroctl/core"
@@ -76,21 +75,21 @@
 	ctx := clicontext.WithInterrupt(context.Background())
 
 	// TODO(lorenz): Have a key management story for this
-	if err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "metroctl"), 0700); err != nil {
+	if err := os.MkdirAll(flags.configPath, 0700); err != nil && !os.IsExist(err) {
 		log.Fatalf("Failed to create config directory: %v", err)
 	}
 
 	var params *api.NodeParameters
 	if bootstrap {
 		var ownerPublicKey ed25519.PublicKey
-		ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
+		ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem"))
 		if os.IsNotExist(err) {
 			pub, priv, err := ed25519.GenerateKey(rand.Reader)
 			if err != nil {
 				log.Fatalf("Failed to generate owner private key: %v", err)
 			}
 			pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
-			if err := os.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"), pemPriv, 0600); err != nil {
+			if err := os.WriteFile(filepath.Join(flags.configPath, "owner-key.pem"), pemPriv, 0600); err != nil {
 				log.Fatalf("Failed to store owner private key: %v", err)
 			}
 			ownerPublicKey = pub
diff --git a/metropolis/cli/metroctl/main.go b/metropolis/cli/metroctl/main.go
index 9224598..7c1e050 100644
--- a/metropolis/cli/metroctl/main.go
+++ b/metropolis/cli/metroctl/main.go
@@ -1,6 +1,9 @@
 package main
 
 import (
+	"path/filepath"
+
+	"github.com/adrg/xdg"
 	"github.com/spf13/cobra"
 )
 
@@ -16,6 +19,8 @@
 	clusterEndpoints []string
 	// proxyAddr is a SOCKS5 proxy address the cluster will be accessed through.
 	proxyAddr string
+	// configPath overrides the default XDG config path
+	configPath string
 }
 
 var flags metroctlFlags
@@ -23,6 +28,7 @@
 func init() {
 	rootCmd.PersistentFlags().StringArrayVar(&flags.clusterEndpoints, "endpoints", nil, "A list of the target cluster's endpoints.")
 	rootCmd.PersistentFlags().StringVar(&flags.proxyAddr, "proxy", "", "SOCKS5 proxy address")
+	rootCmd.PersistentFlags().StringVar(&flags.configPath, "config", filepath.Join(xdg.ConfigHome, "metroctl"), "An alternative cluster config path")
 }
 
 func main() {
diff --git a/metropolis/cli/metroctl/takeownership.go b/metropolis/cli/metroctl/takeownership.go
index b399d17..975bd38 100644
--- a/metropolis/cli/metroctl/takeownership.go
+++ b/metropolis/cli/metroctl/takeownership.go
@@ -10,7 +10,6 @@
 	"os/exec"
 	"path/filepath"
 
-	"github.com/adrg/xdg"
 	"github.com/spf13/cobra"
 	"google.golang.org/grpc"
 	clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication/v1"
@@ -41,7 +40,7 @@
 	clusterEp := flags.clusterEndpoints[0]
 
 	ctx := clicontext.WithInterrupt(context.Background())
-	ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
+	ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem"))
 	if os.IsNotExist(err) {
 		log.Fatalf("Owner key does not exist. takeownership needs to be executed on the same system that has previously installed the cluster using metroctl install.")
 	} else if err != nil {
@@ -77,7 +76,7 @@
 		Type:  "CERTIFICATE",
 		Bytes: ownerCert.Certificate[0],
 	}
-	if err := os.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner.pem"), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(flags.configPath, "owner.pem"), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
 		log.Printf("Failed to store retrieved owner certificate: %v", err)
 		log.Fatalln("Sorry, the cluster has been lost as taking ownership cannot be repeated. Fix the reason the file couldn't be written and reinstall the node.")
 	}