-
Notifications
You must be signed in to change notification settings - Fork 260
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Golang-based deploy-cli which handles bmo and ironic deployments
Signed-off-by: Max Rantil <[email protected]> Signed-off-by: Huy Mai <[email protected]>
- Loading branch information
Showing
11 changed files
with
1,493 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,322 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"fmt" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
|
||
"embed" | ||
"golang.org/x/crypto/bcrypt" | ||
"regexp" | ||
testexec "sigs.k8s.io/cluster-api/test/framework/exec" | ||
"sigs.k8s.io/kustomize/api/krusty" | ||
"sigs.k8s.io/kustomize/kyaml/filesys" | ||
"text/template" | ||
) | ||
|
||
// DeployContext defines the context of the deploy run | ||
type DeployContext struct { | ||
Context context.Context | ||
KubeconfigPath string | ||
// Whether to deploy with basic auth | ||
DeployBasicAuth bool | ||
// Whether to deploy with TLS | ||
DeployTLS bool | ||
// Whether to deploy KeepAlived | ||
DeployKeepAlived bool | ||
// Whether to deploy Mariadb | ||
DeployMariadb bool | ||
// string represents whether to deploy Ironic with RestartContainerCertificateUpdated | ||
RestartContainerCertificateUpdated string | ||
// kustomization overlays | ||
BMOOverlay string | ||
IronicOverlay string | ||
// Endpoint for Ironic | ||
IronicHostIP string | ||
// Username and Password for Ironic authentication | ||
IronicUsername string | ||
IronicPassword string | ||
// Endpoint for Mariadb | ||
MariaDBHostIP string | ||
// Templates to render files using in deployments | ||
TemplateFiles embed.FS | ||
} | ||
|
||
// determineIronicAuth determines the username and password configured for ironic | ||
// authentication, following the order: | ||
// - `IRONIC_USERNAME` and `IRONIC_PASSWORD` env var | ||
// - `ironic-username` and `ironic-password` files content | ||
// - Random string | ||
func (d *DeployContext) determineIronicAuth() error { | ||
ironicDataDir := GetEnvOrDefault("IRONIC_DATA_DIR", "/tmp/metal3/ironic/") | ||
ironicAuthDir := filepath.Join(ironicDataDir, "auth") | ||
ironicUsernameFile := filepath.Join(ironicAuthDir, "ironic-username") | ||
|
||
if err := os.MkdirAll(ironicAuthDir, 0755); err != nil { | ||
return err | ||
} | ||
|
||
ironicUsername, err := getEnvOrFileContent("IRONIC_USERNAME", ironicUsernameFile) | ||
if err != nil && !os.IsNotExist(err) { | ||
return err | ||
} | ||
if ironicUsername == "" { | ||
ironicUsername, err = GenerateRandomString(12) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
if err = os.WriteFile(ironicUsernameFile, []byte(ironicUsername), 0600); err != nil { | ||
return err | ||
} | ||
|
||
ironicPasswordFile := filepath.Join(ironicAuthDir, "ironic-password") | ||
ironicPassword, err := getEnvOrFileContent("IRONIC_PASSWORD", ironicPasswordFile) | ||
if err != nil || ironicPassword == "" { | ||
ironicPassword, err = GenerateRandomString(12) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
if err = os.WriteFile(ironicPasswordFile, []byte(ironicPassword), 0600); err != nil { | ||
return err | ||
} | ||
|
||
d.IronicUsername = ironicUsername | ||
d.IronicPassword = ironicPassword | ||
|
||
return nil | ||
} | ||
|
||
// deployIronic configures the kustomize overlay for ironic | ||
// based on the configuration, then install ironic with that overlay | ||
func (d *DeployContext) deployIronic() error { | ||
ironicOverlay := d.IronicOverlay | ||
|
||
if ironicOverlay == "" { | ||
ironicOverlay, err := MakeRandomDirectory("/tmp/ironic-overlay-", 0755) | ||
if err != nil { | ||
return err | ||
} | ||
ironicKustomizeTpl := "templates/ironic-kustomize.tpl" | ||
kustomizeFile := filepath.Join(ironicOverlay, "kustomization.yaml") | ||
|
||
if err := RenderEmbedTemplateToFile(d.TemplateFiles, ironicKustomizeTpl, kustomizeFile, d); err != nil { | ||
return err | ||
} | ||
|
||
ironicBMOConfigMapTpl := "templates/ironic_bmo_configmap_env.tpl" | ||
ironicBMOConfigMapOutput := filepath.Join(ironicOverlay, "ironic_bmo_configmap.env") | ||
|
||
if err := RenderEmbedTemplateToFile(d.TemplateFiles, ironicBMOConfigMapTpl, ironicBMOConfigMapOutput, d); err != nil { | ||
return err | ||
} | ||
|
||
d.IronicOverlay = ironicOverlay | ||
} | ||
ironicOverlay = d.IronicOverlay | ||
|
||
username, password := d.IronicUsername, d.IronicPassword | ||
|
||
if username != "" && password != "" { | ||
ironicHtpasswd, err := GenerateHtpasswd(username, password) | ||
if err != nil { | ||
return err | ||
} | ||
htpasswdPath := filepath.Join(ironicOverlay, "ironic-htpasswd") | ||
if err = os.WriteFile(htpasswdPath, []byte("IRONIC_HTPASSWD="+ironicHtpasswd), 0600); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return BuildAndApplyKustomization(d.Context, d.KubeconfigPath, d.IronicOverlay) | ||
} | ||
|
||
// deployBMO generates the YAML for the Bare Metal Operator using Kustomize | ||
// and applies it to the Kubernetes cluster. | ||
func (d *DeployContext) deployBMO() error { | ||
bmoOverlay := d.BMOOverlay | ||
|
||
if bmoOverlay == "" { | ||
bmoOverlay, err := MakeRandomDirectory("/tmp/bmo-overlay-", 0755) | ||
if err != nil { | ||
return err | ||
} | ||
kustomizeTpl := "templates/bmo-kustomize.tpl" | ||
kustomizeFile := filepath.Join(bmoOverlay, "kustomization.yaml") | ||
if err := RenderEmbedTemplateToFile(d.TemplateFiles, kustomizeTpl, kustomizeFile, d); err != nil { | ||
return err | ||
} | ||
ironicEnvTpl := "templates/ironic.env.tpl" | ||
ironicEnvFile := filepath.Join(bmoOverlay, "ironic.env") | ||
if err := RenderEmbedTemplateToFile(d.TemplateFiles, ironicEnvTpl, ironicEnvFile, d); err != nil { | ||
return err | ||
} | ||
d.BMOOverlay = bmoOverlay | ||
} | ||
bmoOverlay = d.BMOOverlay | ||
username, password := d.IronicUsername, d.IronicPassword | ||
if username != "" && password != "" { | ||
usernameFile := filepath.Join(bmoOverlay, "ironic-username") | ||
if err := os.WriteFile(usernameFile, []byte(username), 0600); err != nil { | ||
return err | ||
} | ||
passwordFile := filepath.Join(bmoOverlay, "ironic-password") | ||
if err := os.WriteFile(passwordFile, []byte(password), 0600); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return BuildAndApplyKustomization(d.Context, d.KubeconfigPath, d.BMOOverlay) | ||
} | ||
|
||
// cleanup removes temporary files created for basic auth credentials during deployment. | ||
func (d *DeployContext) cleanup() error { | ||
tempFiles := []string{ | ||
filepath.Join(d.BMOOverlay, "ironic-username"), | ||
filepath.Join(d.BMOOverlay, "ironic-password"), | ||
filepath.Join(d.IronicOverlay, "ironic-auth-config"), | ||
filepath.Join(d.IronicOverlay, "ironic-htpasswd"), | ||
} | ||
for _, tempFile := range tempFiles { | ||
if err := os.Remove(tempFile); err != nil && !os.IsNotExist(err) { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// GetEnvOrDefault returns the value of the environment variable key if it exists | ||
// and is non-empty. Otherwise it returns the provided default value. | ||
func GetEnvOrDefault(key, defaultValue string) string { | ||
value, exists := os.LookupEnv(key) | ||
if exists && value != "" { | ||
return value | ||
} | ||
|
||
return defaultValue | ||
} | ||
|
||
// GenerateHtpasswd generates a htpasswd entry for the given username and password. | ||
func GenerateHtpasswd(username, password string) (string, error) { | ||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return fmt.Sprintf("%s:%s", username, string(hashedPassword)), nil | ||
} | ||
|
||
// GenerateRandomString generates random string of given length | ||
// using crypto/rand and base64 encoding. | ||
func GenerateRandomString(length int) (string, error) { | ||
b := make([]byte, length) | ||
_, err := rand.Read(b) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to generate random string: %v", err) | ||
} | ||
|
||
return base64.RawURLEncoding.EncodeToString(b)[:length], nil | ||
} | ||
|
||
// getEnvOrFileContent checks for an environment variable; | ||
// if the env var is not present, reads the content of a file and return | ||
func getEnvOrFileContent(varName, filePath string) (string, error) { | ||
val, exists := os.LookupEnv(varName) | ||
if exists && val != "" { | ||
log.Printf("[%s] Using value from environment variable", varName) | ||
return val, nil | ||
} | ||
content, err := os.ReadFile(filePath) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return string(content), nil | ||
} | ||
|
||
// MakeRandomDirectory generates a new directory whose name starts with the | ||
// provided prefix and ends with a random string | ||
func MakeRandomDirectory(prefix string, perm os.FileMode) (string, error) { | ||
randomStr, err := GenerateRandomString(6) | ||
if err != nil { | ||
return "", err | ||
} | ||
randomDir := prefix + randomStr | ||
if err := os.RemoveAll(randomDir); err != nil { | ||
return "", err | ||
} | ||
if err := os.MkdirAll(randomDir, perm); err != nil { | ||
return "", err | ||
} | ||
return randomDir, nil | ||
} | ||
|
||
// RenderEmbedTemplateToFile reads in a go-template, renders it with supporting data | ||
// and then write the result to an output file | ||
func RenderEmbedTemplateToFile(templateFiles embed.FS, inputFile, outputFile string, data interface{}) error { | ||
tmpl, err := template.ParseFS(templateFiles, inputFile) | ||
if err != nil { | ||
return err | ||
} | ||
f, err := os.Create(outputFile) | ||
if err != nil { | ||
return err | ||
} | ||
defer f.Close() | ||
|
||
if err = tmpl.Execute(f, data); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// GetKubeconfigPath returns the path to the kubeconfig file. | ||
func GetKubeconfigPath() string { | ||
// Check KUBECTL_ARGS env var | ||
kubectlArgs, exists := os.LookupEnv("KUBECTL_ARGS") | ||
if exists { | ||
re := regexp.MustCompile(`--kubeconfig=([^\s]+)`) | ||
match := re.FindStringSubmatch(kubectlArgs) | ||
if len(match) > 1 { | ||
return match[1] | ||
} | ||
} | ||
kubeconfigPath := os.Getenv("KUBECONFIG") | ||
if kubeconfigPath == "" { | ||
kubeconfigPath = os.Getenv("HOME") + "/.kube/config" | ||
} | ||
return kubeconfigPath | ||
} | ||
|
||
// NOTE: The following functions are almost identical to the same functions in BMO e2e | ||
// They can be removed and imported from BMO E2E after the next BMO release | ||
func BuildKustomizeManifest(source string) ([]byte, error) { | ||
kustomizer := krusty.MakeKustomizer(krusty.MakeDefaultOptions()) | ||
fSys := filesys.MakeFsOnDisk() | ||
resources, err := kustomizer.Run(fSys, source) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return resources.AsYaml() | ||
} | ||
|
||
// BuildAndApplyKustomization builds the provided kustomization | ||
// and apply it to the cluster provided by clusterProxy. | ||
func BuildAndApplyKustomization(ctx context.Context, kubeconfigPath string, kustomization string) error { | ||
var err error | ||
manifest, err := BuildKustomizeManifest(kustomization) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err = testexec.KubectlApply(ctx, kubeconfigPath, manifest); err != nil { | ||
return err | ||
} | ||
return nil | ||
} |
Oops, something went wrong.