feat: added support for outputting bootstrap credentials to k8 secret

This commit is contained in:
Sheen Capadngan
2025-06-19 01:43:47 +08:00
parent 59ac14380a
commit 2b8220a71b
5 changed files with 371 additions and 15 deletions
+4 -4
View File
@@ -631,8 +631,8 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
return nil
}
func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) {
var resBody map[string]interface{}
func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (BootstrapInstanceResponse, error) {
var resBody BootstrapInstanceResponse
response, err := httpClient.
R().
SetResult(&resBody).
@@ -641,11 +641,11 @@ func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRe
Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain))
if err != nil {
return nil, NewGenericRequestError(operationCallBootstrapInstance, err)
return BootstrapInstanceResponse{}, NewGenericRequestError(operationCallBootstrapInstance, err)
}
if response.IsError() {
return nil, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil)
return BootstrapInstanceResponse{}, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil)
}
return resBody, nil
+32
View File
@@ -655,3 +655,35 @@ type BootstrapInstanceRequest struct {
Organization string `json:"organization"`
Domain string `json:"domain"`
}
type BootstrapInstanceResponse struct {
Message string `json:"message"`
Identity BootstrapIdentity `json:"identity"`
Organization BootstrapOrganization `json:"organization"`
User BootstrapUser `json:"user"`
}
type BootstrapIdentity struct {
ID string `json:"id"`
Name string `json:"name"`
Credentials BootstrapIdentityCredentials `json:"credentials"`
}
type BootstrapIdentityCredentials struct {
Token string `json:"token"`
}
type BootstrapOrganization struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
type BootstrapUser struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Username string `json:"username"`
SuperAdmin bool `json:"superAdmin"`
}
+200 -10
View File
@@ -4,9 +4,14 @@ Copyright (c) 2023 Infisical Inc.
package cmd
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"text/template"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/util"
@@ -14,6 +19,133 @@ import (
"github.com/spf13/cobra"
)
// handleK8SecretOutput processes the k8-secret output type by creating a Kubernetes secret
func handleK8SecretOutput(bootstrapResponse api.BootstrapInstanceResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace string) error {
// Read Kubernetes service account credentials from the pod
k8sToken, err := os.ReadFile(util.KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
if err != nil {
return fmt.Errorf("failed to read Kubernetes service account token: %v", err)
}
k8sCaCert, err := os.ReadFile(util.KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH)
if err != nil {
return fmt.Errorf("failed to read Kubernetes CA certificate: %v", err)
}
// Get Kubernetes API server URL from environment variables
k8sHost := os.Getenv(util.KUBERNETES_SERVICE_HOST_ENV_NAME)
k8sPort := os.Getenv(util.KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME)
if k8sHost == "" || k8sPort == "" {
return fmt.Errorf("failed to get Kubernetes API server address from environment variables")
}
k8sApiUrl := fmt.Sprintf("https://%s:%s", k8sHost, k8sPort)
// Parse and execute the template to render only the data/stringData section
tmpl, err := template.New("k8-secret-template").Funcs(template.FuncMap{
"b64enc": func(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
},
}).Parse(k8SecretTemplate)
if err != nil {
return fmt.Errorf("failed to parse output template: %v", err)
}
var renderedDataSection bytes.Buffer
err = tmpl.Execute(&renderedDataSection, bootstrapResponse)
if err != nil {
return fmt.Errorf("failed to execute output template: %v", err)
}
// Parse the rendered template as JSON to validate it's valid
var dataSection map[string]interface{}
if err := json.Unmarshal(renderedDataSection.Bytes(), &dataSection); err != nil {
return fmt.Errorf("template output is not valid JSON: %v", err)
}
// Construct the complete Kubernetes secret object
k8sSecret := map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": k8SecretName,
"namespace": k8SecretNamespace,
},
"type": "Opaque",
}
// Merge the rendered data section into the secret
for key, value := range dataSection {
k8sSecret[key] = value
}
// Prepare the HTTP client with TLS configuration
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(k8sCaCert) {
return fmt.Errorf("failed to parse Kubernetes CA certificate")
}
tlsConfig := &tls.Config{
RootCAs: caCertPool,
}
// Create a new HTTP client for Kubernetes API
k8sHttpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
return fmt.Errorf("failed to create Kubernetes HTTP client: %v", err)
}
k8sHttpClient.SetTLSClientConfig(tlsConfig)
k8sHttpClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", string(k8sToken)))
k8sHttpClient.SetHeader("Content-Type", "application/json")
// Check if secret already exists first
checkUrl := fmt.Sprintf("%s/api/v1/namespaces/%s/secrets/%s", k8sApiUrl, k8SecretNamespace, k8SecretName)
checkResponse, err := k8sHttpClient.R().Get(checkUrl)
if err != nil {
return fmt.Errorf("failed to check if Kubernetes secret exists: %v", err)
}
secretUrl := fmt.Sprintf("%s/api/v1/namespaces/%s/secrets", k8sApiUrl, k8SecretNamespace)
if checkResponse.StatusCode() == 200 {
// Secret exists, update it
secretUrl = fmt.Sprintf("%s/%s", secretUrl, k8SecretName)
response, err := k8sHttpClient.R().
SetBody(k8sSecret).
Put(secretUrl)
if err != nil {
return fmt.Errorf("failed to update Kubernetes secret: %v", err)
}
if response.IsError() {
return fmt.Errorf("kubernetes API returned error when updating secret: %s", response.String())
}
log.Info().Msgf("Successfully updated Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace)
} else {
// Secret doesn't exist, create it
response, err := k8sHttpClient.R().
SetBody(k8sSecret).
Post(secretUrl)
if err != nil {
return fmt.Errorf("failed to create Kubernetes secret: %v", err)
}
if response.IsError() {
return fmt.Errorf("kubernetes API returned error when creating secret: %s", response.String())
}
log.Info().Msgf("Successfully created Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace)
}
return nil
}
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: "Used to bootstrap your Infisical instance",
@@ -23,7 +155,7 @@ var bootstrapCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
email, _ := cmd.Flags().GetString("email")
if email == "" {
if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok {
if envEmail, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_EMAIL_NAME); ok {
email = envEmail
}
}
@@ -35,7 +167,7 @@ var bootstrapCmd = &cobra.Command{
password, _ := cmd.Flags().GetString("password")
if password == "" {
if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok {
if envPassword, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_PASSWORD_NAME); ok {
password = envPassword
}
}
@@ -47,7 +179,7 @@ var bootstrapCmd = &cobra.Command{
organization, _ := cmd.Flags().GetString("organization")
if organization == "" {
if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok {
if envOrganization, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_ORGANIZATION_NAME); ok {
organization = envOrganization
}
}
@@ -69,11 +201,55 @@ var bootstrapCmd = &cobra.Command{
return
}
outputType, err := cmd.Flags().GetString("output")
if err != nil {
log.Error().Msgf("Failed to get output type: %v", err)
return
}
k8SecretTemplate, err := cmd.Flags().GetString("k8-secret-template")
if err != nil {
log.Error().Msgf("Failed to get output template: %v", err)
}
k8SecretName, err := cmd.Flags().GetString("k8-secret-name")
if err != nil {
log.Error().Msgf("Failed to get k8-secret-name: %v", err)
}
k8SecretNamespace, err := cmd.Flags().GetString("k8-secret-namespace")
if err != nil {
log.Error().Msgf("Failed to get k8-secret-namespace: %v", err)
}
if outputType == "k8-secret" {
if k8SecretTemplate == "" {
log.Error().Msg("k8-secret-template is required when using k8-secret output type")
return
}
if k8SecretName == "" {
log.Error().Msg("k8-secret-name is required when using k8-secret output type")
return
}
if k8SecretNamespace == "" {
log.Error().Msg("k8-secret-namespace is required when using k8-secret output type")
return
}
}
httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
log.Error().Msgf("Failed to get resty client with custom headers: %v", err)
return
}
ignoreIfBootstrapped, err := cmd.Flags().GetBool("ignore-if-bootstrapped")
if err != nil {
log.Error().Msgf("Failed to get ignore-if-bootstrapped flag: %v", err)
}
httpClient.SetHeader("Accept", "application/json")
bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{
@@ -84,16 +260,26 @@ var bootstrapCmd = &cobra.Command{
})
if err != nil {
log.Error().Msgf("Failed to bootstrap instance: %v", err)
if !ignoreIfBootstrapped {
log.Error().Msgf("Failed to bootstrap instance: %v", err)
}
return
}
responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
if err != nil {
log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
return
if outputType == "k8-secret" {
if err := handleK8SecretOutput(bootstrapResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace); err != nil {
log.Error().Msgf("Failed to handle k8-secret output: %v", err)
return
}
} else {
responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
if err != nil {
log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
return
}
fmt.Println(string(responseJSON))
}
fmt.Println(string(responseJSON))
},
}
@@ -102,6 +288,10 @@ func init() {
bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin")
bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin")
bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance")
bootstrapCmd.Flags().String("output", "", "The type of output to use for the bootstrap command (json or k8-secret)")
bootstrapCmd.Flags().Bool("ignore-if-bootstrapped", false, "Whether to continue on error if the instance has already been bootstrapped")
bootstrapCmd.Flags().String("k8-secret-template", "", "The template to use for rendering the Kubernetes secret (entire secret YAML)")
bootstrapCmd.Flags().String("k8-secret-namespace", "", "The namespace to use for the Kubernetes secret")
bootstrapCmd.Flags().String("k8-secret-name", "", "The name of the Kubernetes secret to create")
rootCmd.AddCommand(bootstrapCmd)
}
+9
View File
@@ -10,6 +10,10 @@ const (
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
INFISICAL_BOOTSTRAP_EMAIL_NAME = "INFISICAL_ADMIN_EMAIL"
INFISICAL_BOOTSTRAP_PASSWORD_NAME = "INFISICAL_ADMIN_PASSWORD"
INFISICAL_BOOTSTRAP_ORGANIZATION_NAME = "INFISICAL_ADMIN_ORGANIZATION"
VAULT_BACKEND_AUTO_MODE = "auto"
VAULT_BACKEND_FILE_MODE = "file"
@@ -47,6 +51,11 @@ const (
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30
INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key"
KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS"
KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
)
var (